roslyn Documentation

The Roslyn .NET compiler provides C# and Visual Basic languages with rich code analysis APIs.

20,746 stars C# C# 98 files · ~187,315 tokens #csharp#hacktoberfest#roslyn#visual-basic#visual-studio
RAW Doc

Technical Documentation: dotnet/roslyn

ℹ️ Provenance: Multi-Tier Ingestion · CodeWiki Reference

Overview (README.md)

The .NET Compiler Platform

Roslyn is the open-source implementation of both the C# and Visual Basic compilers with an API surface for building code analysis tools.

C# and Visual Basic Language Feature Suggestions

If you want to suggest a new feature for the C# or Visual Basic languages go here:

Contributing

All work on the C# and Visual Basic compiler happens directly on GitHub. Both core team members and external contributors send pull requests which go through the same review process.

If you are interested in fixing issues and contributing directly to the code base, a great way to get started is to ask some questions on GitHub Discussions! Then check out our contributing guide which covers the following:

Community

The Roslyn community can be found on GitHub Discussions, where you can ask questions, voice ideas, and share your projects.

To chat with other community members, you can join the Roslyn channel on the CSharp Community Discord.

Our Code of Conduct applies to all Roslyn community channels and has adopted the .NET Foundation Code of Conduct.

Documentation

Visit Roslyn Architecture Overview to get started with Roslyn’s API’s.

NuGet Feeds

The latest pre-release builds are available from the following public NuGet feeds:

  • Compiler: https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-tools/nuget/v3/index.json
  • IDE Services: https://pkgs.dev.azure.com/azure-public/vside/_packaging/vssdk/nuget/v3/index.json
  • .NET SDK: https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet5/nuget/v3/index.json

Continuous Integration status

Builds

Branch Windows Debug Windows Release Unix Debug
main

Desktop Unit Tests

Branch Debug x86 Debug x64 Release x86 Release x64
main

CoreClr Unit Tests

Branch Windows Debug Windows Release Linux
main

Integration Tests

Branch Debug x86 Debug x64 Release x86 Release x64
main

Misc Tests

Branch Determinism Analyzers Build Correctness Source build TODO/Prototype Spanish MacOS
main &configuration=Source-Build+(Managed)&label=build)

.NET Foundation

This project is part of the .NET Foundation along with other projects like the .NET Runtime.


README (`docs/README.md`)

README

This file is a placeholder. This directory will contain public documentation.


Compilers README (`docs/compilers/README.md`)

Roslyn Compiler

Specification

The compiler specification details the supported (and semi-supported) surface area of the Roslyn VB and C# compilers. This includes

  1. Command-line switches and their meaning
  2. Breaking changes from previous versions of the compilers
  3. Compiler behaviors that are (intentionally) contrary to the specification
  4. Compiler features not described by the language specification
    1. COM-specific and other Microsoft-specific features
    2. "Well-known" attributes that affect compiler behavior
    3. The "ruleset" file syntax and semantics
  5. Features included for interoperability between C# and VB, for example
    1. Named Indexers use from C#
  6. Places where the compiler behavior diverges from the language specification
  7. Limitations (e.g. identifier length)
  8. History of language changes per version

The language specification itself is not included here.

Platforms

The compiler is officially supported in the following configurations:

  • .NET Framework: x86, x64 and ARM64
  • .NET Core:
    • Operating Systems: Windows, macOS, Linux
    • Architectures: x86, x64, and ARM64
  • Mono Core: x86 and x64

The compiler takes contributions for other platforms, such as big-endian architectures, but does not officially support the compiler there.


Contributing README (`docs/contributing/README.md`)

Contributing

Guides for contributing to this project


Ide README (`docs/ide/README.md`)

TOC

dev16

16.9 ([release notes](https://docs.microsoft.com/visualstudio/releases/2019/release-notes-v16.9))

  • There is now IntelliSense completion for preprocessor symbols.
  • Solution Explorer now displays the new .NET 5.0 Source Generators.
  • Go To All won't display duplicate results across netcoreapp3.1 and netcoreapp2.0.
  • Quick Info now displays compiler warning IDs or numbers for suppressions.
  • Using directives will now automatically get added when copying and pasting types to a new file.
  • When pressing ; to accept a method from a completion list, IntelliSense will now automatically insert the parentheses along with a semicolon for object creation and method calls.
  • Semantic colorization for C# 9.0 records.
  • Refactoring that removes unnecessary discards.
  • Refactoring that converts a verbatim and regular string to an interpolated string preserving curly braces that were intended to go in the output.
  • Code fix in Visual Basic that removes the shared keyword when you convert methods that are shared to a module.
  • A refactoring that suggests using new(…) in non-contentious scenarios
  • A code fix that removes redundant equality expressions for both C# and Visual Basic
  • The .NET Code Style (IDE) analyzers can now be enforced on build
  • The Syntax Visualizer shows the current foreground color for enhanced colors
  • A new tooltip when hovering over the diagnostic ID for pragma warnings
  • When you type the return key from within a comment the new line is now automatically commented out
  • Inline parameter name hints enhancements
  • .NET Core Debugging with WSL 2

16.8 ([release notes](https://docs.microsoft.com/visualstudio/releases/2019/release-notes-v16.8))

  • Roslyn analyzers are now included in the .NET 5.0 SDK
  • Refactoring that introduces the new C# 9 not pattern matching syntax when a suppression operator is present
  • Inline method refactoring to help replace usages of static, instance, and extension method within a single statement body
  • Code fix to convert instances of typeof to nameof in C# and GetType to NameOf in Visual Basic
  • C# and Visual Basic support for inline parameter name hints that inserts adornments for literals, casted literals, and object instantiations prior to each argument in function calls
  • Refactoring that extracts members from a selected class to a new base class in both C# and Visual Basic
  • Code cleanup has new configuration options that can apply formatting and file header preferences set in your EditorConfig file across a single file or an entire solution
  • Code fix to remove the in keyword where the argument should not be passed by reference
  • Refactoring that introduces the new C#9 pattern combinators and pattern matching suggestions such as converting == to use is where applicable
  • Code fix to make a class abstract when you are trying to write an abstract method in a class that is not abstract
  • IntelliSense completion in DateTime and TimeSpan string literals automatically appear when the first quote is typed
  • Code fix to remove unnecessary pragma suppressions and unnecessary SuppressMessageAttributes
  • Rename and Find All References understands references to symbols within the target string of global SuppressMessageAttributes
  • ByVal fades to say it's not necessary along with a code fix to remove the unnecessary ByVal in Visual Basic
  • Interactive window support for multiple runtimes, such as .NET Framework and .NET Core.
  • Added a new RegisterAdditionalFileAction API that allows analyzer authors to create an analyzer for additional files.

16.7 ([release notes](https://docs.microsoft.com/visualstudio/releases/2019/release-notes-v16.7))

  • There is now a warning and code fix when a suppression operator is present but has no effect
  • A second code fix suggesting the correct negating expression is also available
  • You can now generate properties when generating a constructor in a type
  • Quick Info now displays the diagnostic ID along with a help link
  • There is now a quick action to add a debugger display attribute to a class
  • There is now a code fix for accidental assignments or comparisons to the same variable
  • You can now generate comparison operators for types that implement IComparable
  • You can now generate IEquatable operators when generating .Equals for structs
  • You can now create and assign properties or fields for all unused constructor parameters
  • There is now IntelliSense completion in DateTime and TimeSpan string literals
  • You can now add a parameter within the Change Signature dialog
  • Analyzer authors can now use CompletionProviders for IntelliSense completions when shipping their analyzers with NuGet

16.6 ([release notes](https://docs.microsoft.com/visualstudio/releases/2019/release-notes-v16.6))

  • Add explicit cast code fix
  • Simplify conditional expressions refactoring
  • Convert regular string literals to verbatim string literals (and back) refactoring
  • Bulk configure the severity level of analyzer categories directly through the editor
  • Quick Info style support for XML comments that contain returns and value tags
  • Add file headers to existing files, projects, and solutions with EditorConfig
  • Microsoft Fakes support for .NET Core and SDK-style projects

16.5 ([release notes](https://docs.microsoft.com/visualstudio/releases/2019/release-notes-v16.5))

  • You can now use System.HashCode to implement the GetHashCode method when System.HashCode is available.
  • Refactoring to convert if statements to switch statements or switch expressions.
  • IntelliSense completion for unimported extension methods. You will first need to turn this option on in Tools > Options > Text Editor > C# > Intellisense > and select Show items from unimported namespaces.

16.4 ([release notes](https://docs.microsoft.com/visualstudio/releases/2019/release-notes-v16.4))

  • You can now configure the severity level of a code style rule directly through the editor (through the Ctrl+. menu) or the error list (right-click on the error, warning, or message). This will also update EditorConfig and works for 3rd party analyzers.
  • Find All References now allows you to group by type and member.
  • Refactoring make local function static
  • Code fix to pass variable explicitly in local static function.
  • The Go To Base command can navigate up the inheritance chain. Available in the context (right-click) menu or you can type (Alt+Home).
  • Refactoring to add null checks for all parameters.
  • Methods that have no XML documentation can now automatically inherit XML documentation from the method it is overriding. Place your cursor over the undocumented method that implements a documented interface method. Quick Info will then display the XML documentation from the interface method.

16.3 ([release notes](https://docs.microsoft.com/visualstudio/releases/2019/release-notes-v16.3))

  • Wrap chains of fluent calls with a refactoring
  • Introduce a local variable immediately after writing its initializer
  • Code Analysis page now in project properties. Right click on the project name within the solution explorer and select properties. Select Code Analysis to install analyzer packages and configure when to run code analysis.
  • Now, for users who turn off the completion for unimported types, it's much easier to get it back in the completion list with the new imported type filter added to the IntelliSense toggles.
  • There is now Quick Info style support for XML comments. Place the cursor over the method name. Quick Info will then display the supported styles from the XML comments above the code.
  • Rename a file when renaming an interface, enum, or class. Place the cursor in the class name and type (Ctrl + R,R) to open the Rename dialogue and check the ‘Rename file’ box.
  • There is now Edit and Continue support for multi-targeted projects which includes modules loaded multiple times in the same process on different domains or load contexts. In addition, developers can edit source files even when the containing project is not loaded or the application is running.

16.2 ([release notes](https://docs.microsoft.com/visualstudio/releases/2019/release-notes-v16.2))

  • Bring back Sort Usings as a separate command from Remove Usings. Available under Edit > IntelliSense.
  • Convert a switch statement to a switch expression (verify you are using C# 8 to get the switch expression feature)
  • Generate a parameter
  • Test Explorer improvements
    • Significant reduction in memory consumed by the Visual Studio process and faster test discovery for solutions with large numbers of tests
    • Filter buttoms for passed, failed, not run tests
    • Additional buttons for the 'Run Failed Tests' and 'Run Previous Test Run'
    • Customize what columns are displayed in the Test Explorer
    • Specify what is displayed in each tier of the test hierarchy. The default tiers are Project, Namespace, and then Class, but additional options include Outcome or Duration groupings.
    • The test status window (the pane below the test list that displays the messages, output, etc.) is much more usable. Users can copy substrings of text, and the font-width is fixed for more readable output.
    • Playlists can be displayed in multiple tabs and are much easier to create and discard as needed.
    • Live Unit Testing now has its own view in the Test Explorer. It displays all tests currently included in Live Unit Testing (aka. the live test set), so testers can easily keep track of Live Unit Testing results separate from the manually run test results.
    • There is a target framework column that can display multi-targeted test results.

16.1 ([release notes](https://docs.microsoft.com/visualstudio/releases/2019/release-notes-v16.1))

  • There is now experimental intellisense completion for unimported types! You now receive intellisense suggestions for types in dependencies in your project even if you have not yet added the import statement to your file. You must turn this option on in Tools > Options > Text Editor > C# > Intellisense.
  • Toggle Single Line Comment/Uncomment is now available through the keyboard shortcut (Ctrl+K,/). This command will add or remove a single line comment depending on whether your selection is already commented.
  • You can now export naming styles with the “Generate editorconfig” button located in Tools > Options > Text Editor > C# > Code Style.
  • You can now use a new editorconfig code style rule to require or prevent usings inside a namespace. This setting will also be exported when you use the “Generate editorconfig” button located in Tools > Options > Text Editor > C# > Code Style.
  • The Find All References “Kind” column now has more filter options and is aware of namespaces and types.
  • .NET codefixes and Refactorings
    • Split/Merge if
    • Wrap binary expressions
    • Unseal a class
  • A regex completion list can now be accessed through the intellisense menu (Ctrl + space) when inside a regex string. These completions also include an in-line description of what the suggestion does.
  • You can now use one-click code cleanup for projects and solutions. You can right-click on projects or the solution in the Solution Explorer and select ‘Run Code Cleanup’.
  • You can now use a refactoring dialog to move type to namespace or folder. Place your cursor in the class name and type (Ctrl + .) to open the quick actions and refactorings menu and select ‘Move to namespace.’ This launches a dialog where you can select the target namespace you would like to move the type to.
  • Toggle Block Comment/Uncomment (Ctrl+Shift+/) or through Edit > Advanced > Toggle Block Comment.
  • There is now a codefix for making readonly struct fields writable. Place your cursor in the struct name, type (Ctrl+.) to open the quick actions and refactorings menu, and select ‘Make readonly fields writable.’
  • The codefix to add a private field from a constructor and vice versa is easier to discover and will show when any portion of the field name is selected. This refactoring now also offers all possible constructors.

16.0 ([release notes](https://docs.microsoft.com/visualstudio/releases/2019/release-notes-v16.0))

  • .NET refactorings and codefixes:
    • Sync Namespace and Folder Name
    • Pull members up refactoring with dialog options
    • Wrap/indent/align lists of parameters/arguments
    • Convert anonymous type to tuple
    • Use expression/block body for lambda
    • Invert conditional expressions and logical operations
    • Convert to compound assignment
    • Automatically close block comment on “/”
    • Convert to compound assignment
    • Fix Implicitly-typed variables cannot be constant
    • Auto-fixer to replace @$" with $@" when typing interpolated verbatim string
    • Completion for #nullable enable|disable
    • Fix for unused expression values and parameters
    • Fix for allowing Extract Interface to remain in the same file
    • For cases where "await" is implied but omitted, there is now a compiler warning.
    • Convert a local function to a method.
    • Convert a tuple to a named-struct.
    • Convert an anonymous type to a class.
    • Convert an anonymous type to a tuple.
    • For a foreach loop to LINQ query or to LINQ method.
    • Generate deconstruct method
  • Categorize references by Read/Write in Find All References window.
  • Add Editorconfig when_multiline option for csharp_prefer_braces.
  • New classification colors are available from the .NET Compiler Platform SDK (aka Roslyn). New default colors, similar to the Visual Studio Code colors, are gradually being rolled out. You can adjust these colors in Tools > Options > Environment > Fonts and Colors or turn them off in Environment > Preview Features by unchecking the Use enhanced colors check box. We’d appreciate hearing feedback on how this change affects your workflow.
  • Configure code cleanup (settings formally a part of Format Document)
  • Format document global tool can be run from the command-line
  • Use code metrics with .NET Core projects with our added compatibility.
  • Export editor settings to an Editorconfig file through Tools > Options > Text Editor > C# > Code Style with the button "Generate .editorconfig file from settings".
  • Use C# and Visual Basic's new Regex parser support. Regular expressions are now recognized, and language features are enabled on them. Regex strings are either recognized when a string is passed to the Regex constructor or when a string is immediately preceded with a comment containing the string language=regex. The language features included in this release are classification, brace matching, highlight references, and diagnostics.
  • You can now use dead code analysis for unused private members with an optional code fix to remove unused member declaration.
  • The Find References feature on an accessor now only returns results for that accessor.
  • "Using" statements can be added when code is pasted into a file. A code fix appears after pasting recognized code that prompts you to add relevant missing imports.
  • You can now use Find All References (Shift-F12) and CodeLens to show results from Razor (.cshtml) files in .NET Core projects. You can then navigate to the identified code in the relevant Razor files.
  • You will now receive a warning when running code analysis using FxCop. .NET Compiler analyzers are the recommended way to perform code analysis going forward. Read more on migrating to .NET compiler platform analyzers.

dev15

dev15 Summary

  • Visual Studio-based code cleanup via Format Document
  • Refactorings: InvertIf, add parameter from method callsite, remove unnecessary parentheses, convert if-else assignment, completion for attributes and return to ternary conditional.
  • .editorconfig “refresh” such that changes will be automatically applied without file reload
  • IOperation APIs RTW
  • Portable PDB format to enable cross-platform debugging scenarios

15.9

None

15.8 ([release notes](https://docs.microsoft.com/visualstudio/releasenotes/vs2017-relnotes-v15.8#productivity))

  • Format Document (Ctrl + K, D or Ctrl + E, D) for C# development.
  • .NET refactorings and codefixes:
    • Invert If
    • Add parameter from method callsite
    • Remove unnecessary parentheses
    • Use ternary conditionals in assignments and return statements
  • Go to Enclosing Block (Ctrl + Alt + UpArrow)
  • Go to Next/Previous Issue (Alt + PgUp/PgDn) skip to error, squiggle, lightbulb.
  • Go to Member (Ctrl + T, M) is now scoped to the file by default
  • Multi-caret: Insert carets with Ctrl + Alt + LeftMouseClick.
  • Multi-caret: Add a selection and caret at next location that matches current selection with Shift + Alt + Ins.
  • Access a contextual navigation menu with Alt + `.
  • Visual Studio Code and ReSharper (Visual Studio) keybindings

15.7 ([release notes](https://docs.microsoft.com/visualstudio/releasenotes/vs2017-relnotes-v15.7#dotnet_productivity))

  • .NET refactorings and codefixes:
    • For-to-foreach, and vice versa.
    • Make private fields readonly.
    • Toggle between var and the explicit type, regardless of your code style preferences.
  • Go To Definition (F12) is now supported for LINQ query clauses and deconstructions.
  • Quick Info shows captures on lambdas and local functions, so you can see what variables are in scope.
  • Change Signature refactoring (Ctrl+. on signature) works on local functions.
  • You can edit .NET Core project files in-place, so opening containing folder, restoring tabs, and other Editor features are fully supported. IDE changes, such as adding a linked file, will be merged with unsaved changes in the editor.

15.6 ([release notes](https://docs.microsoft.com/visualstudio/releasenotes/vs2017-relnotes-v15.6#productivity))

  • Navigate to decompiled sources (must enable in Tools > Options > Text Editor > C# > Advanced > Enable navigation to decompiled sources)
  • .NET EditorConfig options: dotnet_prefer_inferred_tuple_names
  • .NET EditorConfig options: dotnet_prefer_inferred_anonymous_type_member_names
  • Ctrl + D to Duplicate text
  • Expand Selection command that allows you to successively expand your selection to the next logical block. You can use the shortcuts Shift+Alt+= to expand and Shift+Alt+- to contract the current selection.

15.5

None

15.4 ([release notes](https://docs.microsoft.com/visualstudio/releasenotes/vs2017-relnotes-v15.4#editor))

  • Editor: Control Click Go To Definition

Roslyn Analyzers README (`docs/roslyn-analyzers/README.md`)

Roslyn Analyzers

Microsoft.CodeAnalysis.Analyzers

Latest stable version:

Latest pre-release version: here

This package contains rules for correct usage of APIs from the Microsoft.CodeAnalysis NuGet package, i.e. .NET Compiler Platform ("Roslyn") APIs. These are primarily aimed towards helping authors of diagnostic analyzers and code fix providers to invoke the Microsoft.CodeAnalysis APIs in a recommended manner. More info about rules in this package

Roslyn.Diagnostics.Analyzers

Latest stable version:

Latest pre-release version: here

This package contains rules that are very specific to the .NET Compiler Platform ("Roslyn") project, i.e. dotnet/roslyn repo. This analyzer package is not intended for general consumption outside the Roslyn repo. More info about rules in this package

Microsoft.CodeAnalysis.BannedApiAnalyzers

Latest stable version:

Latest pre-release version: here

This package contains customizable rules for identifying references to banned APIs. More info about rules in this package

For instructions on using this analyzer, see Instructions.

Microsoft.CodeAnalysis.PublicApiAnalyzers

Latest stable version:

Latest pre-release version: here

This package contains rules to help library authors monitoring change to their public APIs. More info about rules in this package

For instructions on using this analyzer, see Instructions.


Wiki README (`docs/wiki/README.md`)

Getting Started

Language Design

Tools

API Changelist

Other Topics

Contributors

Tool Authors

Status

.NET Core

Interactive and Scripting

Testing


Wiki Roslyn Overview (`docs/wiki/Roslyn-Overview.md`)

.NET Compiler Platform ("Roslyn") Overview

Download in PDF

Contents

Summary

Introduction

Traditionally, compilers are black boxes -- source code goes in one end, magic happens in the middle, and object files or assemblies come out the other end. As compilers perform their magic, they build up deep understanding of the code they are processing, but that knowledge is unavailable to anyone but the compiler implementation wizards. The information is promptly forgotten after the translated output is produced.

For decades, this world view has served us well, but it is no longer sufficient. Increasingly we rely on integrated development environment (IDE) features such as IntelliSense, refactoring, intelligent rename, “Find all references,” and “Go to definition” to increase our productivity. We rely on code analysis tools to improve our code quality and code generators to aid in application construction. As these tools get smarter, they need access to more and more of the deep code knowledge that only compilers possess. This is the core mission of Roslyn: opening up the black boxes and allowing tools and end users to share in the wealth of information compilers have about our code. Instead of being opaque source-code-in and object-code-out translators, through Roslyn, compilers become platforms—APIs that you can use for code related tasks in your tools and applications.

The transition to compilers as platforms dramatically lowers the barrier to entry for creating code focused tools and applications. It creates many opportunities for innovation in areas such as meta-programming, code generation and transformation, interactive use of the C# and VB languages, and embedding of C# and VB in domain specific languages.

Roslyn SDK Preview includes the latest drafts of new language object models for code generation, analysis, and refactoring. We hope to include drafts of API support for scripting and interactive use of C# and Visual Basic in a future preview. This document provides a conceptual overview of Roslyn. Further details can be found in the walkthroughs and samples included in the SDK Preview.

Exposing the Compiler APIs

Compiler Pipeline Functional Areas

Roslyn exposes the C# and Visual Basic compiler’s code analysis to you as a consumer by providing an API layer that mirrors a traditional compiler pipeline.

Each phase of this pipeline is now a separate component. First the parse phase, where source is tokenized and parsed into syntax that follows the language grammar. Second the declaration phase, where declarations from source and imported metadata are analyzed to form named symbols. Next the bind phase, where identifiers in the code are matched to symbols. Finally, the emit phase, where all the information built up by the compiler is emitted as an assembly.

Corresponding to each of those phases, an object model that allows access to the information at that phase is surfaced. The parsing phase is exposed as a syntax tree, the declaration phase as a hierarchical symbol table, the binding phase as a model that exposes the result of the compiler’s semantic analysis and the emit phase as an API that produces IL byte codes.

Each compiler combines these components together as a single end-to-end whole.

To ensure that the public Compiler APIs are sufficient for building world-class IDE features, the language services that will be used to power the C# and VB experiences in Visual Studio vNext have been rebuilt using them. For instance, the code outlining and formatting features use the syntax trees, the Object Browser and navigation features use the symbol table, refactorings and Go to Definition use the semantic model, and Edit and Continue uses all of these, including the Emit API. These experiences may be previewed on Visual Studio 2013 through the “Roslyn” End-User Preview. This preview is required in order to build and test applications build on top of Roslyn SDK meant for integration into Visual Studio though Roslyn APIs can be used in your own applications independently of Visual Studio without requiring the End-User Preview.

API Layers

Roslyn consists of two main layers of APIs – the Compiler APIs and Workspaces APIs.

Compiler APIs

The compiler layer contains the object models that correspond with information exposed at each phase of the compiler pipeline, both syntactic and semantic. The compiler layer also contains an immutable snapshot of a single invocation of a compiler, including assembly references, compiler options, and source code files. There are two distinct APIs that represent the C# language and the Visual Basic language. These two APIs are similar in shape but tailored for high-fidelity to each individual language. This layer has no dependencies on Visual Studio components.

Diagnostic APIs

As part of their analysis the compiler may produce a set of diagnostics covering everything from syntax, semantic, and definite assignment errors to various warnings and informational diagnostics. The Compiler API layer exposes diagnostics through an extensible API allowing for user-defined analyzers to be plugged into a Compilation and user-defined diagnostics, such as those produced by tools like StyleCop or FxCop, to be produced alongside compiler-defined diagnostics. Producing diagnostics in this way has the benefit of integrating naturally with tools such as MSBuild and Visual Studio which depend on diagnostics for experiences such as halting a build based on policy, showing live squiggles in the editor and suggesting code fixes.

Scripting APIs

As a part of the compiler layer, the team created hosting/scripting APIs for executing code snippets and accumulating a runtime execution context. The REPL uses these APIs.

Workspaces APIs

The Workspaces layer contains the Workspace API, which is the starting point for doing code analysis and refactoring over entire solutions. It assists you in organizing all the information about the projects in a solution into single object model, offering you direct access to the compiler layer object models without needing to parse files, configure options or manage project to project dependencies.

In addition, the Workspaces layer surfaces a set of commonly used APIs used when implementing code analysis and refactoring tools that function within a host environment like the Visual Studio IDE, such as the Find All References, Formatting, and Code Generation APIs.

This layer has no dependencies on Visual Studio components.

Working with Syntax

The most fundamental data structure exposed by the Compiler APIs is the syntax tree. These trees represent the lexical and syntactic structure of source code. They serve two important purposes:

  1. To allow tools - such as an IDE, add-ins, code analysis tools, and refactorings - to see and process the syntactic structure of source code in a user’s project.
  2. To enable tools - such as refactorings and an IDE - to create, modify, and rearrange source code in a natural manner without having use direct text edits. By creating and manipulating trees, tools can easily create and rearrange source code.

Syntax Trees

Syntax trees are the primary structure used for compilation, code analysis, binding, refactoring, IDE features, and code generation. No part of the source code is understood without it first being identified and categorized into one of many well-known structural language elements.

Syntax trees have three key attributes. The first attribute is that syntax trees hold all the source information in full fidelity. This means that the syntax tree contains every piece of information found in the source text, every grammatical construct, every lexical token, and everything else in between including whitespace, comments, and preprocessor directives. For example, each literal mentioned in the source is represented exactly as it was typed. The syntax trees also represent errors in source code when the program is incomplete or malformed, by representing skipped or missing tokens in the syntax tree.

This enables the second attribute of syntax trees. A syntax tree obtained from the parser is completely round-trippable back to the text it was parsed from. From any syntax node, it is possible to get the text representation of the sub-tree rooted at that node. This means that syntax trees can be used as a way to construct and edit source text. By creating a tree you have by implication created the equivalent text, and by editing a syntax tree, making a new tree out of changes to an existing tree, you have effectively edited the text.

The third attribute of syntax trees is that they are immutable and thread-safe. This means that after a tree is obtained, it is a snapshot of the current state of the code, and never changes. This allows multiple users to interact with the same syntax tree at the same time in different threads without locking or duplication. Because the trees are immutable and no modifications can be made directly to a tree, factory methods help create and modify syntax trees by creating additional snapshots of the tree. The trees are efficient in the way they reuse underlying nodes, so the new version can be rebuilt fast and with little extra memory.

A syntax tree is literally a tree data structure, where non-terminal structural elements parent other elements. Each syntax tree is made up of nodes, tokens, and trivia.

Syntax Nodes

Syntax nodes are one of the primary elements of syntax trees. These nodes represent syntactic constructs such as declarations, statements, clauses, and expressions. Each category of syntax nodes is represented by a separate class derived from SyntaxNode. The set of node classes is not extensible.

All syntax nodes are non-terminal nodes in the syntax tree, which means they always have other nodes and tokens as children. As a child of another node, each node has a parent node that can be accessed through the Parent property. Because nodes and trees are immutable, the parent of a node never changes. The root of the tree has a null parent.

Each node has a ChildNodes method, which returns a list of child nodes in sequential order based on its position in the source text. This list does not contain tokens. Each node also has a collection of Descendant methods - such as DescendantNodes, DescendantTokens, or DescendantTrivia - that represent a list of all the nodes, tokens, or trivia that exist in the sub-tree rooted by that node.

In addition, each syntax node subclass exposes all the same children through strongly typed properties. For example, a BinaryExpressionSyntax node class has three additional properties specific to binary operators: Left, OperatorToken, and Right. The type of Left and Right is ExpressionSyntax, and the type of OperatorToken is SyntaxToken.

Some syntax nodes have optional children. For example, an IfStatementSyntax has an optional ElseClauseSyntax. If the child is not present, the property returns null.

Syntax Tokens

Syntax tokens are the terminals of the language grammar, representing the smallest syntactic fragments of the code. They are never parents of other nodes or tokens. Syntax tokens consist of keywords, identifiers, literals, and punctuation.

For efficiency purposes, the SyntaxToken type is a CLR value type. Therefore, unlike syntax nodes, there is only one structure for all kinds of tokens with a mix of properties that have meaning depending on the kind of token that is being represented.

For example, an integer literal token represents a numeric value. In addition to the raw source text the token spans, the literal token has a Value property that tells you the exact decoded integer value. This property is typed as Object because it may be one of many primitive types.

The ValueText property tells you the same information as the Value property; however this property is always typed as String. An identifier in C# source text may include Unicode escape characters, yet the syntax of the escape sequence itself is not considered part of the identifier name. So although the raw text spanned by the token does include the escape sequence, the ValueText property does not. Instead, it includes the Unicode characters identified by the escape.

Syntax Trivia

Syntax trivia represent the parts of the source text that are largely insignificant for normal understanding of the code, such as whitespace, comments, and preprocessor directives.

Because trivia are not part of the normal language syntax and can appear anywhere between any two tokens, they are not included in the syntax tree as a child of a node. Yet, because they are important when implementing a feature like refactoring and to maintain full fidelity with the source text, they do exist as part of the syntax tree.

You can access trivia by inspecting a token’s LeadingTrivia or TrailingTrivia collections. When source text is parsed, sequences of trivia are associated with tokens. In general, a token owns any trivia after it on the same line up to the next token. Any trivia after that line is associated with the following token. The first token in the source file gets all the initial trivia, and the last sequence of trivia in the file is tacked onto the end-of-file token, which otherwise has zero width.

Unlike syntax nodes and tokens, syntax trivia do not have parents. Yet, because they are part of the tree and each is associated with a single token, you may access the token it is associated with using the Token property.

Like syntax tokens, trivia are value types. The single SyntaxTrivia type is used to describe all kinds of trivia.

Spans

Each node, token, or trivia knows its position within the source text and the number of characters it consists of. A text position is represented as a 32-bit integer, which is a zero-based Unicode character index. A TextSpan object is the beginning position and a count of characters, both represented as integers. If TextSpan has a zero length, it refers to a location between two characters.

Each node has two TextSpan properties: Span and FullSpan.

The Span property is the text span from the start of the first token in the node’s sub-tree to the end of the last token. This span does not include any leading or trailing trivia.

The FullSpan property is the text span that includes the node’s normal span, plus the span of any leading or trailing trivia.

For example:

csharp
if (x > 3)
      {
||        // this is bad
          |throw new Exception("Not right.");|  // better exception?||
      }

The statement node inside the block has a span indicated by the single vertical bars (|). It includes the characters +throw new Exception(“Not right.”);+. The full span is indicated by the double vertical bars (||). It includes the same characters as the span and the characters associated with the leading and trailing trivia.

Kinds

Each node, token, or trivia has a RawKind property, of type System.Int32, that identifies the exact syntax element represented. This value can be cast to a language-specific enumeration; each language, C# or VB, has a single SyntaxKind enumeration that lists all the possible nodes, tokens, and trivia elements in the grammar. This conversion can be done automatically by accessing the CSharpSyntaxKind() or VisualBasicSyntaxKind() extension methods.

The RawKind property allows for easy disambiguation of syntax node types that share the same node class. For tokens and trivia, this property is the only way to distinguish one type of element from another.

For example, a single BinaryExpressionSyntax class has Left, OperatorToken, and Right as children. The Kind property distinguishes whether it is an AddExpression, SubtractExpression, or MultiplyExpression kind of syntax node.

Errors

Even when the source text contains syntax errors, a full syntax tree that is round-trippable to the source is exposed. When the parser encounters code that does not conform to the defined syntax of the language, it uses one of two techniques to create a syntax tree.

First, if the parser expects a particular kind of token, but does not find it, it may insert a missing token into the syntax tree in the location that the token was expected. A missing token represents the actual token that was expected, but it has an empty span, and its IsMissing property returns true.

Second, the parser may skip tokens until it finds one where it can continue parsing. In this case, the skipped tokens that were skipped are attached as a trivia node with the kind SkippedTokens.

Working with Semantics

Syntax trees represent the lexical and syntactic structure of source code. Although this information alone is enough to describe all the declarations and logic in the source, it is not enough information to identify what is being referenced.

For example, many types, fields, methods, and local variables with the same name may be spread throughout the source. Although each of these is uniquely different, determining which one an identifier actually refers to often requires a deep understanding of the language rules.

There are program elements represented in source code, and programs can also refer to previously compiled libraries, packaged in assembly files. Although no source code is available for assemblies and therefore no syntax nodes or trees, programs can still refer to elements inside them.

In addition to a syntactic model of the source code, a semantic model encapsulates the language rules, giving you an easy way to make these distinctions.

Compilation

A compilation is a representation of everything needed to compile a C# or Visual Basic program, which includes all the assembly references, compiler options, and source files.

Because all this information is in one place, the elements contained in the source code can be described in more detail. The compilation represents each declared type, member, or variable as a symbol. The compilation contains a variety of methods that help you find and relate the symbols that have either been declared in the source code or imported as metadata from an assembly.

Similar to syntax trees, compilations are immutable. After you create a compilation, it cannot be changed by you or anyone else you might be sharing it with. However, you can create a new compilation from an existing compilation, specifying a change as you do so. For example, you might create a compilation that is the same in every way as an existing compilation, except it may include an additional source file or assembly reference.

Symbols

A symbol represents a distinct element declared by the source code or imported from an assembly as metadata. Every namespace, type, method, property, field, event, parameter, or local variable is represented by a symbol.

A variety of methods and properties on the Compilation type help you find symbols. For example, you can find a symbol for a declared type by its common metadata name. You can also access the entire symbol table as a tree of symbols rooted by the global namespace.

Symbols also contain additional information that the compiler determined from the source or metadata, such as other referenced symbols. Each kind of symbol is represented by a separate interface derived from ISymbol, each with its own methods and properties detailing the information the compiler has gathered. Many of these properties directly reference other symbols. For example, the ReturnType property of the IMethodSymbol class tells you the actual type symbol that the method declaration referenced.

Symbols present a common representation of namespaces, types, and members, between source code and metadata. For example, a method that was declared in source code and a method that was imported from metadata are both represented by an IMethodSymbol with the same properties.

Symbols are similar in concept to the CLR type system as represented by the System.Reflection API, yet they are richer in that they model more than just types. Namespaces, local variables, and labels are all symbols. In addition, symbols are a representation of language concepts, not CLR concepts. There is a lot of overlap, but there are many meaningful distinctions as well. For instance, an iterator method in C# or Visual Basic is a single symbol. However, when the iterator method is translated to CLR metadata, it is a type and multiple methods.

Semantic Model

A semantic model represents all the semantic information for a single source file. You can use it to discover the following:

  • The symbols referenced at a specific location in source.
  • The resultant type of any expression.
  • All diagnostics, which are errors and warnings.
  • How variables flow in and out of regions of source.
  • The answers to more speculative questions.

Working with a Workspace

The Workspaces layer is the starting point for doing code analysis and refactoring over entire solutions. Within this layer, the Workspace API assists you in organizing all the information about the projects in a solution into single object model, offering you direct access to compiler layer object models like source text, syntax trees, semantic models and compilations without needing to parse files, configure options or manage inter-project dependencies.

Host environments, like an IDE, provide a workspace for you corresponding to the open solution. It is also possible to use this model outside of an IDE by simply loading a solution file.

Workspace

A workspace is an active representation of your solution as a collection of projects, each with a collection of documents. A workspace is typically tied to a host environment that is constantly changing as a user types or manipulates properties.

The workspace provides access to the current model of the solution. When a change in the host environment occurs, the workspace fires corresponding events, and the CurrentSolution property is updated. For example, when the user types in a text editor corresponding to one of the source documents, the workspace uses an event to signal that the overall model of the solution has changed and which document was modified. You can then react to those changes by analyzing the new model for correctness, highlighting areas of significance, or by making a suggestion for a code change.

You can also create stand-alone workspaces that are disconnected from the host environment or used in an application that has no host environment.

Solutions, Projects, Documents

Although a workspace may change every time a key is pressed, you can work with the model of the solution in isolation.

A solution is an immutable model of the projects and documents. This means that the model can be shared without locking or duplication. After you obtain a solution instance from the Workspace’s CurrentSolution property, that instance will never change. However, like with syntax trees and compilations, you can modify solutions by constructing new instances based on existing solutions and specific changes. To get the workspace to reflect your changes, you must explicitly apply the changed solution back to the workspace.

A project is a part of the overall immutable solution model. It represents all the source code documents, parse and compilation options, and both assembly and project-to-project references. From a project, you can access the corresponding compilation without needing to determine project dependencies or parse any source files.

A document is also a part of the overall immutable solution model. A document represents a single source file from which you can access the text of the file, the syntax tree, and the semantic model.

The following diagram is a representation of how the Workspace relates to the host environment, tools, and how edits are made.

Summary

Roslyn exposes a set of Compiler APIs and Workspaces APIs that provides rich information about your source code and that has full fidelity with the C# and Visual Basic languages. The transition to compilers as a platform dramatically lowers the barrier to entry for creating code focused tools and applications. It creates many opportunities for innovation in areas such as meta-programming, code generation and transformation, interactive use of the C# and VB languages, and embedding of C# and VB in domain specific languages.


Compilers CSharp Readme (`docs/compilers/CSharp/readme.md`)

Breaking changes (newest release on top):


Compilers Design README (`docs/compilers/Design/README.md`)

Design Docs

This directory is a place for design documents for the Roslyn compilers.


Adding Optional Parameters In Public API (`docs/Adding Optional Parameters in Public API.md`)

Adding optional parameters to public methods

Roslyn has strict requirements for maintaining source and binary API compatibility across minor version releases.
Adding overloads of existing methods can be an effective way of adding to the public API without breaking
backwards compatibility. However, overloads involving optional parameters can be tricky to make both source
and binary compatible. The following steps should be followed when adding optional parameters to public methods.

Steps for adding optional parameters to an existing method

  1. Check to see if there is already a method marked with the comment

    text
    // <Previous release> BACKCOMPAT OVERLOAD -- DO NOT TOUCH
  2. If it does not:

    1. Copy the method and make any existing optional parameters mandatory.
    2. Change the body to call the original method.
    3. Add the comment // BACKCOMPAT OVERLOAD -- DO NOT TOUCH.
      This method provides binary compatibility with assemblies compiled against the previous APIs.
  3. Add your new parameter with a default value to the end of the original method.
    This provides source compatibility to consumers of the previous API.

Examples

Before:

csharp
public void O(string o1 = null, string o2 = null)
    {
    }

After:

csharp
public void O(string o1 = null, string o2 = null, bool o3 = false)
    {
    }

    // 1.0 BACKCOMPAT OVERLOAD -- DO NOT TOUCH
    public void O(string o1, string o2)
    {
        O(o1, o2, o3: false);
    }

Things to avoid

DO NOT add a parameter in the middle of existing optional parameters

csharp
public void O(string o1 = null, bool o4 = false, string o2 = null, bool o3 = false, bool o5 = false)
    {
    }

This breaks source compat when certain arguments to a method call are skipped, like O(null, null, o5: true);

DO NOT add multiple overloads with optional parameters

csharp
public void O(string o1 = null, string o2 = null, bool o3 = false)
    {
    }

    // 1.0 BACKCOMPAT OVERLOAD -- DO NOT TOUCH
    public void O(string o1 = null, string o2 = null)
    {
         O(o1, o2, o3: false);
    }

Aside from the fact that this is often ambiguous and it won’t compile, it makes verifying
compatibility much harder.

End Result

The resulting code should 1) have only one overload with optional parameters, which 2) has the
most parameters of all the overloads, and 3) all previous overloads should still exist,
properly commented with their release version, and contain only mandatory parameters.

NOTE

After this change, if Public API Analyzer complains about it, you should copy the entry for your change from PublicAPI.Shipped.txt and then put that entry in PublicAPI.Unshipped.txt with *REMOVED* prefix.

PublicAPI.Shipped.txt

txt
Example.O(string o1 = null, string o2 = null) -> void

PublicAPI.Unshipped.txt

txt
*REMOVED*Example.O(string o1 = null, string o2 = null) -> void

Breaking API Changes (`docs/Breaking API Changes.md`)

API Breaking Changes

Version 1.1.0

Removed VisualBasicCommandLineParser.ctor

During a toolset update we noticed the constructor on VisualBasicCommandLineParser was public. This in turn made many of the protected members of CommandLineParser a part of the API surface as it gave external customers an inheritance path.

It was never the intent for these members to be a part of the supported API surface. Creation of the parsers is meant to be done via the Default singleton properties. There seems to be little risk that we broke any customers here and hence we decided to remove this API.

PR: https://github.com/dotnet/roslyn/pull/4169

Changed Simplifier methods to throw ArgumentNullExceptions

Changed Simplifier.ReduceAsync, Simplifier.ExpandAsync, and Simplifier.Expand methods to throw ArgumentNullExceptions if any non-optional, nullable arguments are passed in. Previously the user would get a NullReferenceException for synchronous methods and an AggregateException containing a NullReferenceException for asynchronous methods.

PR: https://github.com/dotnet/roslyn/pull/5144

Version 1.3.0

Treat a method marked with both public and private flags as private

The scenario is loading an assembly where some methods, fields or nested types have accessibility flags set to 7 (all three bits set), which mean public AND private.
After the fix, such flags are loaded to mean private.
The compat change is we’re trading a compile-time success and runtime failure (native compiler) against a compile-time error (restoring the behavior of v1.2).

Details below:

  • The native compiler successfully compiles the method and field case (those only yield runtime error System.TypeLoadException: Invalid Field Access Flags) and reported an accessibility error on the nested type.
  • The 1.2 compiler generated errors:
text
error BC30390: 'C.Private Overloads Sub M()' is not accessible in this context because it is 'Private'.
error BC30389: 'C.F' is not accessible in this context because it is 'Private'.
error BC30389: 'C.C2' is not accessible in this context because it is 'Protected Friend'.
error BC30390: 'C2.Private Overloads Sub M2()' is not accessible in this context because it is 'Private'.
  • The 1.3 compiler crashes.
  • After fix, the same errors as 1.2 are generated again.

PR: https://github.com/dotnet/roslyn/pull/11547

Don't emit bad DateTimeConstant, and load bad BadTimeConstant as default value instead

The change affects compatibility in two ways:

  • When loading an invalid DateTimeConstant(-1), the compiler will use default(DateTime) instead, whereas the native compiler would produce code that fails to execute.
  • DateTimeConstant(-1) will still count when we check that you don’t specify two default values. The compiler will produce an error, instead of succeeding (and producing IL with two attributes).

PR: https://github.com/dotnet/roslyn/pull/11536

Version 4.1.0

Can no longer inherit from CompletionService and CompletionServiceWithProviders

The constructors of Microsoft.CodeAnalysis.Completion and Microsoft.CodeAnalysis.Completion.CompletionServiceWithProviders are now internal.
Roslyn does not support implementing completion for arbitrary languages.

Version 4.2.0

Can no longer inherit from QuickInfoService

The constructors of Microsoft.CodeAnalysis.QuickInfoService are now internal.
Roslyn does not support implementing completion for arbitrary languages.

`Microsoft.CodeAnalysis.CodeStyle.NotificationOption` is now immutable

All property setters now throw an exception.

Version 4.4.0

Workspace.OnWorkspaceFailed is no longer called when an error occurs while reading source file content from disk.

The Workspace and DocumentId parameters of TextLoader.LoadTextAndVersionAsync(Workspace, DocumentId, CancellationToken) are deprecated.
The method now receives null Workspace and DocumentId.

Version 4.5.0

SymbolDisplayFormat.CSharpErrorMessageFormat and CSharpShortErrorMessageFormat now include parameter names by default if used on a standalone IParameterSymbol.
For example, parameter p in void M(ref int p) was previously formatted as "ref int" and now it is formatted as "ref int p".

Version 4.7.0

`SymbolDisplayFormat` includes parameter name when invoked on `IParameterSymbol`

All SymbolDisplayFormats (predefined and user-created) now include parameter names by default if used on a standalone IParameterSymbol for consistency with predefined formats (see the breaking change for version 4.5.0 above).

Changed `IncrementalStepRunReason` when a modified input produced a new output

IncrementalGeneratorRunStep.Outputs previously contained IncrementalStepRunReason.Modified as Reason
when the input to the step was modified in a way that produced a new output.
Now the reason will be reported more accurately as IncrementalStepRunReason.New.

Version 4.8.0

Changed `Assembly.Location` behavior in non-Windows

The value of Assembly.Location previously held the location on disk where an analyzer or source generator was loaded from. This could be either the original location or the shadow copy location. In 4.8 this will be "" in certain cases when running on non Windows platforms. This is due the compiler server loading assemblies using AssemblyLoadContext.LoadFromStream instead of loading from disk.

This could already happen in other load scenarios but this change moves it into mainline build scenarios.

Deprecation warning for SyntaxNode serialization

The ability to serialize/deserialize a SyntaxNode to/from a Stream has been deprecated. The code for this still exists in Roslyn, but attempting to call the APIs to perform these functions will result in 'Obsolete' warnings being reported. A future version of Roslyn will remove this functionality entirely. This functionality could only work for a host that wrote out the nodes to a stream, and later read it back in within the same process instance. It could not be used to communicate across processes, or for persisting nodes to disk to be read in at a later time by a new host sessions. This functionality originally existed for the days when Roslyn was hosted in 32bit processes with limited address space. That is no longer a mainline supported scenario. Clients can get similar functionality by persisting the text of the node, and parsing it back out when needed.

PR: https://github.com/dotnet/roslyn/pull/70365

Version 4.9.0

Obsoletion and removal of SyntaxNode serialization.

Continuation of the deprecation that happened in 4.8.0 (see information above). In 4.9.0 this functionality is now entirely removed, and will issue both an obsoletion error, and will throw at runtime if the APIs are used.

PR: https://github.com/dotnet/roslyn/pull/70277

Changes in `Microsoft.CodeAnalysis.Emit.EmitBaseline.CreateInitialBaseline` method

A new required parameter Compilation has been added. Existing overloads without this parameter no longer work and throw NotSupportedException.

Changes in `Microsoft.CodeAnalysis.Emit.SemanticEdit` constructors

The value of preserveLocalVariables passed to the constructors is no longer used.

Version 5.11.0

Changed the `default` value of `Microsoft.CodeAnalysis.CSharp.Conversion`

The default value of Microsoft.CodeAnalysis.CSharp.Conversion struct now returns false for all boolean properties. Previously the Exists and IsExplicit properties returned true:

cs
using System;
using Microsoft.CodeAnalysis.CSharp;

var conv = default(Conversion);
Console.WriteLine(conv.Exists); // Previously was 'True', now 'False'
Console.WriteLine(conv.IsExplicit); // Previously was 'True', now 'False'

PR: https://github.com/dotnet/roslyn/pull/84628


Language Feature Status (`docs/Language Feature Status.md`)

Language Feature Status

This document reflects the status, and planned work in progress, for the compiler team. It is a live document
and will be updated as work progresses, features are added / removed, and as work on feature progresses.
This is not an exhaustive list of our features but rather the ones which have active development
efforts behind them.

The "Feature" column contains a link to the championed proposal or doc for the feature.
The "State" column contains a link to the test plan issue for the feature. The test plan should have a "Feature - XYZ" label, which is also used on PRs and issues for the feature.
To add a new feature entry (along with its test plan issue and label), use the new-compiler-feature skill.

Working Set C#

Merged features should be in the bottom portion of the table (below the break), while in-progress features should be in the top portion of the table.
When a feature is merged to the main branch, its entry should be moved to the very bottom of the table and its "State" value should be updated to record the relevant version number.

Feature Branch State Developer Reviewer IDE Buddy LDM Champ
Dictionary expressions dictionary-expressions In progress 333fred RikkiGibson, jjonescz CyrusNajmabadi CyrusNajmabadi
Null-conditional await null-conditional-await In progress CyrusNajmabadi TBD TBD CyrusNajmabadi
Chained relational comparison chained-relational-comparison In progress CyrusNajmabadi TBD TBD CyrusNajmabadi
Target-typed static member access target-typed-static-member-access In progress CyrusNajmabadi TBD TBD CyrusNajmabadi
Relax modifier ordering relaxed-modifier-ordering In progress CyrusNajmabadi TBD TBD CyrusNajmabadi
Compound assignment in initializers compound-assignment-in-initializer In progress CyrusNajmabadi TBD TBD CyrusNajmabadi
Extension members on typeless receivers extension-members-on-typeless-receivers In progress CyrusNajmabadi TBD TBD CyrusNajmabadi
Runtime Async Streams runtime-async-streams Test plan for "runtime-async" jcouv 333fred, RikkiGibson
Extension constants extension-consts In progress jcouv TBD TBD jcouv
Type Parameter Inference from Constraints PR In progress agocke 333fred, jjonescz TBD 333fred
------- ------ ----- --------- -------- --------- ---------
Unsafe evolution UnsafeEvolution Merged as preview feature into .NET 11p2 and VS 18.6 jjonescz 333fred, jcouv TBD agocke
Runtime Async main Main feature merged into main in preview 333fred jcouv, RikkiGibson

Working Set VB

C# 15.0

Feature Branch State Developer Reviewer IDE Buddy LDM Champ
Collection expression arguments collection-expression-arguments C# 15 CyrusNajmabadi 333fred, jcouv CyrusNajmabadi CyrusNajmabadi
Unions Unions C# 15 AlekseyTs RikkiGibson, jjonescz TBD MadsTorgersen
Non-virtual static interface members without DIM runtime support main C# 15 AlekseyTs 333fred, RikkiGibson TBD N/A
ExtendedLayoutAttribute main Merged into 18.3 jkoritzinsky 333fred, jcouv
Closed class hierarchies closed-class C# 15 RikkiGibson AlekseyTs, jjonescz TBD mattwar
Extension indexers extensions C# 15 jcouv AlekseyTs, CyrusNajmabadi CyrusNajmabadi jcouv
Labeled break/continue labeled-break-and-continue C# 15 CyrusNajmabadi jcouv, jjonescz jcouv CyrusNajmabadi

C# 14.0

Feature Branch State Developer Reviewer IDE Buddy LDM Champ
field keyword in properties field-keyword Merged into 17.12p3 Youssef1313, cston 333fred, RikkiGibson CyrusNajmabadi CyrusNajmabadi
First-class Span Types FirstClassSpan Merged into 17.13p1 jjonescz cston, 333fred 333fred, stephentoub
Unbound generic types in nameof PR Merged into 17.13p2 CyrusNajmabadi jcouv, AlekseyTs CyrusNajmabadi CyrusNajmabadi
String literals in data section as UTF8 PR Merged into 17.13p4 jjonescz AlekseyTs, cston N/A N/A
Simple lambda parameters with modifiers PR Merged into 17.14p1 CyrusNajmabadi jjonescz, cston CyrusNajmabadi CyrusNajmabadi
Partial Events and Constructors PartialEventsCtors Merged into 17.14p3 jjonescz cston, RikkiGibson CyrusNajmabadi jaredpar
Extensions extensions Merged into 17.14p3 jcouv, AlekseyTs jjonescz, TBD CyrusNajmabadi MadsTorgersen
Null-conditional assignment null-conditional-assignment Merged into 17.14p3 RikkiGibson cston, jjonescz CyrusNajmabadi RikkiGibson
Ignored directives PR Merged into 17.14p3 jjonescz RikkiGibson, jaredpar CyrusNajmabadi jaredpar
User Defined Compound Assignment Operators UserDefinedCompoundAssignment Merged to main AlekseyTs 333fred, cston CyrusNajmabadi AlekseyTs
Optional and named arguments in Expression trees PR Merged to main(https://github.com/dotnet/roslyn/issues/76934) cston 333fred, AlekseyTs CyrusNajmabadi cston

VB in .NET 10

Feature Branch State Developer Reviewer IDE Buddy LDM Champ
Recognizing 'unmanaged' constraint main Merged into 17.13 P2 AlekseyTs cston, 333fred (no IDE impact) jaredpar
Overload Resolution Priority main Merged into 17.13 P3 AlekseyTs 333fred, cston (no IDE impact) 333fred

C# 13.0

Feature Branch State Developer Reviewer IDE Buddy LDM Champ
Escape character N/A Merged into 17.9p1 CyrusNajmabadi jcouv, RikkiGibson (no IDE impact) CyrusNajmabadi
Method group natural type improvements main Merged into 17.9p2 jcouv AlekseyTs, cston (no IDE impact) jcouv
Lock object LockObject Merged into 17.10p2 jjonescz cston, RikkiGibson CyrusNajmabadi (needs IDE fixer) stephentoub
Implicit indexer access in object initializers main Merged into 17.9p3 jcouv AlekseyTs, cston (no IDE impact)
Params-collections main Merged to 17.10p3 AlekseyTs RikkiGibson, 333fred akhera99 (needs IDE fixer) MadsTorgersen, AlekseyTs
Ref/unsafe in iterators/async RefInAsync Merged into 17.11p2 jjonescz AlekseyTs, cston (no IDE impact)
Overload Resolution Priority main Merged to 17.12p1 333fred jcouv, cston Not yet assigned 333fred
Partial properties partial-properties Merged into 17.11p3 RikkiGibson jcouv, 333fred Cosifne 333fred, RikkiGibson
Ref Struct Interfaces/allows ref struct constraint RefStructInterfaces Merged into 17.11p2 AlekseyTs cston, jjonescz ToddGrun agocke, jaredpar
Collection expression better conversion from expression main Merged into 17.12p3 333fred cston, AlekseyTs (no IDE impact) 333fred, CyrusNajmabadi

C# 12.0

Feature Branch State Developer Reviewer LDM Champ
ref readonly parameters RefReadonly Merged into 17.8p2 jjonescz jcouv, AlekseyTs akhera99
Collection Expressions CollectionLiterals Merged into 17.7p5 cston 333fred, RikkiGibson CyrusNajmabadi
Interceptors (experimental feature) interceptors Merged into 17.7p3 RikkiGibson cston, jcouv akhera99
Inline Arrays InlineArrays Merged into 17.7p3 AlekseyTs cston, jjonescz Cosifne
nameof accessing instance members main Merged into 17.7p1 YairHalberstadt, jjonescz 333fred, AlekseyTs 333fred
Using aliases for any type main Merged into 17.6.P3 CyrusNajmabadi jcouv cston CyrusNajmabadi
Primary Constructors PrimaryConstructors Merged into 17.6.P2 AlekseyTs cston, jjonescz akhera99
Lambda optional parameters lambda-default-parameters Merged into 17.5p2 adamperlin, jjonescz 333fred, cston captainsafia
Experimental attribute main Merged into 17.8p2 jcouv cston, jjonescz jcouv

C# 11.0

Feature Branch State Developer Reviewer LDM Champ
File-local types file-types Merged into 17.4p1 RikkiGibson jcouv, cston stephentoub
ref fields ref-fields Merged into 17.3p3 cston RikkiGibson, AlekseyTs jaredpar
Required members main Merged into 17.3p2 333fred jcouv, RikkiGibson 333fred
DIM for Static Members DefaultInterfaceImplementation Merged into 17.3p2 AlekseyTs 333fred, jcouv MadsTorgersen
Numeric IntPtr numeric-intptr Merged into 17.3p2 jcouv cston, 333fred jcouv
Unsigned Right Shift UnsignedRightShift Merged into 17.3p1 AlekseyTs 333fred, jcouv AlekseyTs
Utf8 String Literals main Merged into 17.3p1 AlekseyTs cston, RikkiGibson MadsTorgersen
Pattern matching on ReadOnlySpan<char> patterns-span-char Merged into 17.3p1 YairHalberstadt cston, RikkiGibson jcouv
Checked Operators CheckedUserDefinedOperators Merged into 17.3p1 AlekseyTs 333fred, chsienki AlekseyTs
auto-default structs main Merged into 17.3p1 RikkiGibson cston, jcouv
Newlines in interpolations main Merged in 17.1p1 CyrusNajmabadi jcouv, chsienki CyrusNajmabadi
List patterns main Merged in 17.1p2 alrz jcouv, 333fred 333fred
Raw string literals main Merged into 17.2 CyrusNajmabadi jcouv CyrusNajmabadi
Cache delegates for static method group main Merged into 17.2 pawchen AlekseyTs, jcouv AlekseyTs
nameof(parameter) main Merged into 17.3p2 jcouv AlekseyTs, RikkiGibson jcouv
Relaxing Shift Operator main Merged into 17.3p2 AlekseyTs 333fred, jcouv MadsTorgersen
Generic attributes main Merged into 17.0p4 AviAvni RikkiGibson, jcouv mattwar

C# 10.0

Feature Branch State Developer Reviewer LDM Champ
Record structs main Merged into 16.11 jcouv AlekseyTs, RikkiGibson jcouv
Global Using Directive main Merged into 16.11 AlekseyTs 333fred, cston AlekseyTs
Improved Definite Assignment main Merged into 17.0 RikkiGibson jcouv jaredpar
Constant Interpolated Strings main Merged into 16.9p3 kevinsun-dev 333fred jaredar, agocke
Extended property patterns main Merged into 17.0 alrz jcouv, 333fred 333fred
Sealed record ToString main Merged thomaslevesque jcouv 333fred
Source Generator V2 APIs main Merged into 17.0p2 chsienki rikkigibson, jaredpar, cston N/A
Mix declarations and variables in deconstruction main Merged into 16.10 YairHalberstadt jcouv MadsTorgersen
Async method builder override main Merged into 17.0p2 jcouv cston, RikkiGibson stephentoub
Enhanced #line directive main Merged into 17.0p2 cston jcouv, RikkiGibson MadsTorgersen
Lambda improvements main Merged into 17.0p2 cston 333fred, jcouv jaredpar
Static Abstract Members In Interfaces C# 10 Preview main Merged into 17.0p2 AlekseyTs 333fred, RikkiGibson MadsTorgersen
Interpolated string improvements main Merged into 17.0p3 333fred AlekseyTs, chsienki jaredpar
File-scoped namespace main Merged into 17.0p3 RikkiGibson jcouv, chsienki CyrusNajmabadi
Parameterless struct constructors main Merged into 17.0p3 cston jcouv, 333fred jcouv
Caller expression attribute main Merged into 17.0p3 Youssef1313 333fred,AlekseyTs jcouv

VB 17.0

Feature Branch State Developer Reviewer LDM Champ
Caller expression attribute main Merged into 17.0p3 Youssef1313 333fred,AlekseyTs jcouv

VB 16.9

Feature Branch State Developer Reviewer LDM Champ
Enable consumption of init-only properties main Merged (16.9p3) AlekseyTs jcouv N/A

C# 9

Feature Branch State Developer Reviewer LDM Champ
Target-typed new target-typed-new Merged into 16.7p1 alrz jcouv jcouv
Skip locals init localsinit Merged t-camaia, agocke jaredpar agocke
Lambda discard parameters main Merged jcouv AlekseyTs, chsienki jcouv
Native ints main Merged into 16.7p1 cston 333fred, gafter jaredpar
Attributes on local functions local-function-attributes Merged RikkiGibson agocke agocke
Function pointers main Merged into 16.7p3 333fred AlekseyTs jaredpar
Pattern matching improvements main Merged into 16.7p1 gafter RikkiGibson,agocke gafter
Static lambdas main Merged in 16.7p4 CyrusNajmabadi jcouv jcouv
Records main Merged into 16.7p3 agocke gafter, 333fred agocke
Target-typed conditional target-typing Merged into 16.7p4 gafter agocke, RikkiGibson gafter
Covariant Returns covariant-returns Merged into 16.8p2 gafter AlekseyTs, agocke gafter
Extension GetEnumerator main Merged into 16.8p2 YairHalberstadt 333fred 333fred
Module initializers main Merged in 16.7p4 RikkiGibson jnm2 AlekseyTs gafter
Extending Partial main Merged into 16.7p2 RikkiGibson chsienki jaredpar
Top-level statements SimplePrograms Merged into 16.7p3 AlekseyTs cston, RikkiGibson MadsTorgersen

C# 8.0

Feature Branch State Developer Reviewer LDM Champ
Default Interface Methods main Merged to dev16.1 preview2 AlekseyTs gafter gafter
Nullable reference type main Merged to dev16.0 preview1 cston, jcouv AlekseyTs, 333fred mattwar
Recursive patterns main Merged to dev16.0 preview2 gafter agocke, cston gafter
Async streams main Merged to dev16.0 preview1 jcouv agocke stephentoub
Enhanced using main Merged to dev16.0 preview2 chsienki agocke jaredpar
Ranges main Merged to dev16.0 preview1 agocke cston jaredpar
Null-coalescing Assignment main Merged to dev16.0 preview1 333fred cston gafter
Alternative interpolated verbatim strings main Merged to dev16.0 preview1 jcouv cston jcouv
stackalloc in nested contexts main Merged to dev16.2 preview1 gafter - gafter
Unmanaged generic structs main Merged to dev16.1 preview1 RikkiGibson - jaredpar
Static local functions main Merged in dev16.0 preview2 cston jaredpar jcouv
Readonly members main Merged to dev16.1 preview2 RikkiGibson TBD jaredpar

VB 16.0

Feature Branch State Developer Reviewer LDM Champ
Line continuation comments main Merged in 16.1 (preview2) paul1956 AlekseyTs gafter
Relax null-coalescing operator requirements main Merged in 16.0 333fred cston gafter

C# 7.3

Feature Branch State Developer Reviewer LDM Champ
blittable None Merged None - jaredpar
Support == and != for tuples main Merged jcouv AlekseyTs jcouv
strongname main Merged tyoverby agocke jaredpar
Attribute on backing field features/compiler Merged jcouv AlekseyTs jcouv
Ref Reassignment main Merged agocke vsadov gafter
Constraints main Merged OmarTawfik vsadov gafter
Stackalloc initializers main Merged alrz vsadov jcouv
Custom fixed main Merged vsadov jcouv jarepdar
Indexing movable fixed buffers - Merged vsadov jcouv jarepdar
Improved overload candidates features/compiler Merged gafter cston mattwar
Expression variables main Merged AlekseyTs gafter AlekseyTs

C# 7.2 fixes

Feature Branch State Developer Reviewer LDM Champ
ref and this ordering in ref extension - Merged alrz - -
Tiebreaker for by-val and in overloads main Merged OmarTawfik - -

C# 7.2

Feature Branch State Developer Reviewer LDM Champ
ref readonly main Merged vsadov, OmarTawfik cston,gafter jaredpar
interior pointer/Span/ref struct main Merged vsadov gafter, jaredpar jaredpar
non-trailing named arguments main Merged jcouv gafter jcouv
private protected main Merged gafter jcouv gafter
conditional ref operator main Merged vsadov cston jaredpar
Digit separator after base specifier main Merged alrz - gafter

C# 7.1

Feature Branch State Developer Reviewer LDM Champ
Async Main main Merged tyoverby vsadov stephentoub
Default Expressions main Merged jcouv cston jcouv
Ref Assemblies main Merged (IDE and project-system integrations ongoing) jcouv gafter N/A
Infer tuple names main Merged jcouv gafter jcouv
Pattern-matching with generics main Merged gafter agocke gafter

FAQ

  • Is target version a guarantee?: No. It's explicitly not a guarantee. This is just the planned and ongoing work to the best of our knowledge at this time.
  • Where are these State values defined?: Take a look at the Developing a Language Feature document.

Layering (`docs/Layering.md`)

Roslyn Architecture - Project Layering

Arrows point from a project up to its dependencies.

text
/* Detailed source-code truncated for AI context efficiency. */

TestAccessor Pattern (`docs/TestAccessor Pattern.md`)

The `TestAccessor` Pattern

Prefer testing observable behavior through existing product entry points. Do not
introduce a TestAccessor for exhaustive unit testing or to assert a class's
specific implementation. When a scenario is critical to cover and cannot be
adequately tested through existing observable behavior, the TestAccessor
pattern allows production code to expose the minimum required internal
functionality without making it available to other production code.

The pattern has two primary components:

  1. A TestAccessor type, which contains the functionality available only for testing
  2. A GetTestAccessor() method, which returns an instance of TestAccessor

The pattern relies on enforcement of a simple rule that no production code is allowed to call a GetTestAccessor() method. This is enforceable either through code reviews or through an analyzer. This pattern has many advantages over alternatives:

  • The pattern does not require expanded accessibility (e.g. internal instead of private) for the purpose of testing
  • The pattern is self-documenting: all properties and methods within a TestAccessor type are intended only for use in test code
  • The pattern is consistent enough to enforce through static analysis (analyzers)
  • The pattern is simple enough to enforce manually (code reviews)

The `TestAccessor` Type

The TestAccessor type is typically defined as a nested structure. In the following example, the SomeProductionType.TestAccessor.PrivateStateData property allows test code to read and write the value of the private field SomeProductionType._privateStateData without exposing the _privateStateData field to other production code.

csharp
internal class SomeProductionType
{
  private int _privateStateData;

  internal readonly struct TestAccessor
  {
    private readonly SomeProductionType _instance;

    internal TestAccessor(SomeProductionType instance)
    {
      _instance = instance;
    }

    internal ref int PrivateStateData => ref _instance._privateStateData;
  }
}

The `GetTestAccessor()` Method

The GetTestAccessor() method is always defined as follows:

csharp
internal TestAccessor GetTestAccessor()
  => new TestAccessor(this);

Area Owners (`docs/area-owners.md`)

Pull Requests Tagging

If you need to tag folks on an issue or PR, you will generally want to tag the owners (not the lead) for area to which the change or issue is closest to.

Areas

Area Lead Owners
api-Syntax @jaredpar @CyrusNajmabadi @333fred
api-Symbols @jaredpar @AlekseyTs
api-SemanticModel @jaredpar @AlekseyTs @333fred
api-IOperation @jaredpar @333fred
api-SourceGenerators @jaredpar @chsienki
api-Analyzers @jaredpar @mavasani @333fred
api-CodeFixes (Refactoring) @jinuj @mavasani @sharwell
api-Completion @jinuj @genlu @CyrusNajmabadi
api-Debugging @jinuj @tmat @davidwengier
api-EditAndContinue @jinuj @tmat @davidwengier
api-EmbeddedLanguages @jinuj @CyrusNajmabadi @joerobich
api-Formatting @jinuj @sharwell @joerobich
api-LSP @jinuj @dibarbet @jasonmalinowski
api-MetadataAsSource @jinuj @sharwell @jasonmalinowski
api-Navigation @jinuj @CyrusNajmabadi @dibarbet
api-QuickInfo @jinuj @sharwell @jasonmalinowski
api-SignatureHelp @jinuj @sharwell @jasonmalinowski
api-Structure (Outlining) @jinuj @CyrusNajmabadi @jasonmalinowski
api-Tagging (Classification) @jinuj @CyrusNajmabadi @joerobich
api-Workspace @jinuj @jasonmalinowski @tmat

Roslyn Language Server Copilot Plugin (`docs/roslyn-language-server-copilot-plugin.md`)

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). It is distributed as a plugin through the dotnet/skills marketplace, enabling automatic installation and use in tools like GitHub Copilot CLI.

Quick Start

For this repository specifically, the dotnet skill from the dotnet-agent-skills marketplace is checked in at .github/copilot/settings.json, which includes the appropriate lsp.json. The .vscode/settings.json points the server at Roslyn.slnx. That means opening this repo in Copilot CLI is enough to have the C# LSP configured automatically.

Install the plugin

In your agent (Copilot CLI, etc.), add the marketplace and install the dotnet plugin:

text
/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 server is configured to automatically discover and load projects via either the --autoLoadProjects [maximum] CLI flag (defaults to a maximum of 500 projects if value omitted) or LSP initializationOptions in the Copilot lsp.json as defined below.

jsonc
// lsp.json
{
  "initializationOptions": { "autoLoadProjects": 500 }
}

For how the server chooses which solution or projects to load, and how to control it, see Automatic project loading.

Troubleshooting

Verify LSP configuration is found

  • In Copilot CLI, running the /lsp show should show the server configured for C#:
text
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.

How It Works

The plugin is configured in the dotnet/skills repository via plugins/dotnet/lsp.json:

When the agent opens a workspace containing .cs files, it will:

  1. Install and run the 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 [maximum] Automatically discover and load projects; optionally limit individual project discovery (currently defaults to 500)
--logLevel <level> Minimum log verbosity (default: Information)
--debug Launch the debugger on startup

Analyzers Analyzer Actions Semantics (`docs/analyzers/Analyzer Actions Semantics.md`)

Definitions:

  • Session: For batch compilation, a single compilation. For a hosted environment, an arbitrary duration potentially encompassing many compilations. In particular, for VS, a session shares the lifetime and scope of an analyzer reference, which may correspond to a loaded project, a loaded solution, or the VS process.
  • Analyzer: An instance of a type derived from DiagnosticAnalyzer.
  • Action: A method instance registered via a Register***Action method of an instance of a ***Context class, to be automatically applied as appropriate. Every action is associated with an analyzer and has a kind (which is one of compilation start, syntax tree, symbol, code block start, syntax node, code block, code block end, semantic model, compilation, compilation end).
  • Operation block: A single unit of executable code containing operation nodes, e.g. a method body, parameter default value, field initializer, etc. Operation blocks are primarily used for semantic analysis of executable code using the IOperation APIs.
  • Code block: A single unit of executable code containing syntax nodes, e.g. a method body, parameter default value, field initializer, etc. Code blocks are primarily used for syntax and/or semantic analysis of executable code using the language specific Syntax APIs. However, it is recommended that analyzers use Operation block and IOperation API for semantic analysis of executable code for improved performance and simpler, language-agnostic implementation.
  • Symbol members: List of zero or member symbols for a given ISymbol. For a namespace or type symbol, these are the members returned by INamespaceOrTypeSymbol.GetMembers API, which include nested symbols. Method, field, event and property symbols have no member symbols.

Axioms:

  • Two different analyzers are assumed not to share state. No actions of a single analyzer execute concurrently, unless they explicitly configure concurrent execution in the DiagnosticAnalyzer.Initialize method by invoking AnalysisContext.EnableConcurrentExecution. Actions of different analyzers may execute concurrently. (This is likely to be refined as we develop more sophisticated concurrency strategies, but it will remain true that actions will generally not require locks to avoid races.)
  • The context used to register an action affects the lifetime of the registration but does not otherwise affect the semantics of applying the action.
  • If an analyzer has multiple applicable actions of any given kind, all of those actions are applied in an arbitrary order.

Ordering of actions:

The DiagnosticAnalyzer.Initialize method of an analyzer is invoked before any actions of an analyzer can be applied. The actions registered by Initialize methods form the initial set of actions used for each compilation. Initialize method may be invoked multiple times on an analyzer instance in a session, but is guaranteed to be invoked only once for any given compilation.

The following rules related to action invocation apply per compilation or per any environmental change that invalidates prior analysis (e.g. changes to analyzer options or changes to non-compiled documents used by analyzers). For purposes of this specification, such environmental changes act as if they create a new compilation.

There are no ordering constraints for the invocations of any actions, except those introduced by start and end actions (compilation start, compilation end, symbol start, symbol end, operation block start, operation block end, code bock start, and code block end actions). Invocations of actions of one kind may be interleaved with invocations of actions of another kind. A given host environment might appear to provide a predictable order—for example, invoking syntax tree actions before semantic model actions, or symbol actions before code block actions, but depending on such an ordering is not safe.

  • A compilation start action is invoked once, before any of the analyzer’s other kinds of actions are invoked. Actions registered by a compilation start action apply to that compilation only.
  • A syntax tree action is invoked once per source document. A syntax tree action can be expected to be invoked as early as possible after a document is parsed, but this is not guaranteed.
  • A symbol action is invoked once per complete semantic processing of a declaration of a type or type member, provided that symbol has a kind matching one of the kinds supplied when the action was registered.
  • A symbol start action is invoked exactly once for every symbol of registered SymbolKind, before any of the analyzer’s other kinds of actions are invoked on the symbol or the symbol's members. A symbol start action can register following actions:
    • Actions for syntax and operation nodes within the symbol and its members: Operation action, operation block action, operation block start action, syntax node action, code block action and code block start action.
    • Symbol end action for the symbol, after all other applicable actions of an analyzer have been invoked on the symbol and its members.
  • An operation block start action is invoked once per operation block, before any operation node actions applicable to nodes within the block. Actions registered by a operation block start action apply to that operation block only.
  • An operation action is invoked once per operation node, provided that the operation node has a kind matching one of the kinds supplied when the action was registered.
  • An operation block action is invoked once per operation block.
  • An operation block end action is invoked once per operation block, after invoking any operation node actions applicable to nodes within the block and any operation block actions for the block.
  • A code block start action is invoked once per code block, before any syntax node actions applicable to nodes within the block. Actions registered by a code block start action apply to that code block only.
  • A syntax node action is invoked once per syntax node, provided that the syntax node has a kind matching one of the kinds supplied when the action was registered.
  • A code block action is invoked once per code block.
  • A code block end action is invoked once per code block, after invoking any syntax node actions applicable to nodes within the block and any code block actions for the block.
  • A semantic model action is invoked once per source document.
  • A compilation action is invoked once.
  • A compilation end action is invoked once, after all other applicable actions of an analyzer have been invoked.

Summarizing the ordering rules:

  • For a given compilation, compilation start actions run first and compilation end actions run last.
  • For a given symbol, symbol start actions run before operation/syntax/symbol actions on a symbol and its members, and these operation/syntax/symbol actions run before the symbol end actions.
  • For a given operation block, operation block start actions run before operation node actions, and operation node actions run before the operation block end actions.
  • For a given code block, code block start actions run before syntax node actions, and syntax node actions run before code block end actions.
  • Otherwise, there are no ordering guarantees for action invocation.

Implementation freedom:

If an implementation determines that a given action will have equivalent effects in two compilations within a session, it can omit invoking the action for the second compilation and instead retain the effects of invoking it for the first compilation. For example, in an IDE, edits to a source document will not necessarily cause actions to be invoked for unrelated symbols etc. defined in other documents.

A host environment may delay invocation of actions arbitrarily and indefinitely. For example, an IDE might delay invoking some actions until a significant pause in editing activity, during which time several compilations might be initiated. Some actions might not execute at all for the interim compilations, because they will be cancelled when the next compilation begins. In such cases, it is possible for a compilation start action to run without its corresponding compilation end actions ever running for a given compilation, and likewise a code block start action can run without its corresponding code block end actions ever running. Therefore, end actions cannot be relied upon to free resources acquired by start actions.


Analyzers Analyzer Samples (`docs/analyzers/Analyzer Samples.md`)

Samples Location:

Sample analyzers to demonstrate recommended implementation models for different analysis scenarios have been added to Samples.sln.

Description:

Analyzers have been broadly categorized into the following three buckets based on the kind of analysis performed:

  1. Stateless analyzers: Analyzers that report diagnostics about a specific code unit, such as a symbol, syntax node, code block, compilation, etc. Most analyzers would fall into this bucket.
    These analyzers register one or more actions, that:
    1. Do not require maintaining state across analyzer actions and are
    2. Independent of the order of execution of individual analyzer actions.
  2. Stateful analyzers: Analyzers that report diagnostics about a specific code unit, but in context of an enclosing code unit such as a code block or a compilation. These represent slightly more complicated analyzers that require more powerful and wider analysis and hence need careful design to achieve efficient analyzer execution without memory leaks.
    These analyzers require at least one of the following kind of state manipulation for analysis:
    1. Access to immutable state object(s) for the enclosing code unit, such as a compilation or the code block. For example, access to certain well known types defined in a compilation.
    2. Perform analysis over the enclosing code unit, with mutable state defined and initialized in a start action for the enclosing code unit, intermediate actions that access and/or update this state, and an end action to report diagnostic on the individual code units.
  3. Additional File analyzers: Analyzers that read data from non-source text files included in the project.

Contents:

Following sample analyzers, with simple unit tests, are provided:

  1. Stateless analyzers:
    1. SymbolAnalyzer: Analyzer for reporting symbol diagnostics.
    2. SyntaxNodeAnalyzer: Analyzer for reporting syntax node diagnostics.
    3. CodeBlockAnalyzer: Analyzer for reporting code block diagnostics.
    4. CompilationAnalyzer: Analyzer for reporting compilation diagnostics.
    5. SyntaxTreeAnalyzer: Analyzer for reporting syntax tree diagnostics.
    6. SemanticModelAnalyzer: Analyzer for reporting syntax tree diagnostics, that require some semantic analysis.
  2. Stateful analyzers:
    1. CodeBlockStartedAnalyzer: Analyzer to demonstrate code block wide analysis.
    2. CompilationStartedAnalyzer: Analyzer to demonstrate analysis within a compilation, for example analysis that depends on certain well-known symbol(s).
    3. CompilationStartedAnalyzerWithCompilationWideAnalysis: Analyzer to demonstrate compilation-wide analysis.
  3. Additional File analyzers:
    1. SimpleAdditionalFileAnalyzer: Demonstrates reading an additional file line-by-line and using the data in analysis.
    2. XmlAdditionalFileAnalyzer: Demonstrates writing an additional file out to a Stream so that it can be read back as a structured document (in this case, XML).

Analyzers DiagnosticSuppressorDesign (`docs/analyzers/DiagnosticSuppressorDesign.md`)

Feature Request

https://github.com/dotnet/roslyn/issues/30172: Programmatic suppression of warnings

Provide an ability for platform/library authors to author simple, context-aware compiler extensions to programmatically suppress specific instances of reported analyzer and/or compiler diagnostics, which are always known to be false positives in the context of the platform/library.

Programmatically Suppressible Diagnostic

An analyzer/compiler diagnostic would be considered a candidate for programmatic suppression if all of the following conditions hold:

  1. Not an error by default: Diagnostic's DefaultSeverity is not DiagnosticSeverity.Error.
  2. Must be configurable: Diagnostic is not tagged with WellKnownDiagnosticTags.NotConfigurable custom tag, indicating that its severity is configurable.
  3. No existing source suppression: Diagnostic is not already suppressed in source via pragma/suppress message attribute.

Core Design

  1. For platform/library authors: Expose a new "DiagnosticSuppressor" public API for authoring such compiler extensions. API contract for the suppressors (detailed API with doc comments in the last section):
    1. Declaratively provide all the analyzer and/or compiler diagnostic IDs that can be suppressed by it.
    2. For each such suppressible diagnostic ID, provide a unique suppression ID and justification. These are required to enable proper diagnosis and configuration of each suppression (covered in detail later).
    3. Register callbacks for reported analyzer and/or compiler diagnostics with these suppressible diagnostic IDs. The callback can analyze the syntax and/or semantics of the diagnostic location and report a suppression for it.
    4. DiagnosticSuppressors would not be able to register any other analysis callbacks, and hence cannot report any new diagnostics.
  2. For end users:
    1. Seamless development experience when targeting such a platform/library, whereby the users do not see false positives from analyzers/compiler in their development context.
    2. End users do not need to manually add and/or maintain source suppressions through pragmas/SuppressMessageAttributes for such context-specific false positives.
    3. End users still have the ultimate control over suppressors (specifics covered in later section):
      1. Audit: Diagnostic suppressions are logged as "Info" diagnostics on the command line, so the end users can audit each suppression in the verbose build log or msbuild binlog. Additionally, they are also logged in the /errorlog SARIF file as suppressed diagnostics.
      2. Configuration: Each diagnostic suppression has an associated suppression ID. This is clearly indicated in the logged "Info" diagnostics for suppressions, and the end users can disable the bucket of suppressions under any specific suppression ID with simple command line argument or ruleset entries.
  3. For analyzer driver:
    1. Order of execution between analyzers/compiler and suppressors: For command line builds, all diagnostic suppressors will run after all the analyzer and compiler diagnostics have been computed. For live analysis in Visual Studio, diagnostic suppressors may be invoked with a subset of the full set of reported diagnostics, as an optimization for supporting incremental and partial analysis scenarios.
    2. Order of execution between individual suppressors: Execution of diagnostic suppressors would be independent of other suppressors. Diagnostic suppressions reported by one suppressor would not affect the input set of reported diagnostics passed to any other suppressor. Each diagnostic might be programmatically suppressed by more than one suppressor, so the analyzer driver will union all the reported suppressions for each diagnostic. Command line compiler will log one suppression diagnostic per-suppression for each programmatically suppressed diagnostic.

Development Experience Example

Let us consider the core example for UnityEngine covered in https://github.com/dotnet/roslyn/issues/30172:

csharp
using UnityEngine;

class BulletMovement : MonoBehaviour
{
    [SerializeField] private float speed;

    private void Update()
    {
         this.transform.position *= speed;
    }
}

Serialization frameworks and tools embedding .NET Core or Mono often set the value of fields or call methods outside of user code. Such code is not available for analysis to the compiler and analyzers, leading to false reports such as following:

  1. CS0649: Field 'speed' is never assigned to, and will always have its default value 0
  2. IDE0044 dotnet_style_readonly_field: Make field 'speed' readonly

New experience with screenshots:

  1. For platform/library authors: UnityEngine can selectively suppress instances of such diagnostics by writing a simple SerializedFieldDiagnosticSuppressor extension based on the DiagnosticSuppressor API and packaging it along with the Unity framework SDK/library.
  2. For end users:
    1. Users do not see above false positives in the IDE live analysis or command line build. For example, see the below screenshots with and without the SerializedFieldDiagnosticSuppressor:
      1. Without the suppressor: See image here
      2. With the suppressor: See image here
    2. Audit suppressions:
      1. Command line build: End users will see "Info" severity suppression diagnostics in command line builds, see image here
        info SPR0001: Diagnostic 'CS0649' was programmatically suppressed by a DiagnosticSuppressor with suppresion ID 'SPR1001' and justification 'Field symbols marked with SerializedFieldAttribute are implicitly assigned by UnityEngine at runtime and hence do not have its default value'
        These diagnostics will always be logged in the msbuild binlog. They would not be visible in the console output for regular msbuild invocations, but increasing the msbuild verbosity to detailed or diagnostic will emit these diagnostics.
      2. Visual Studio IDE: Users can see the original suppressed diagnostic in the Visual Studio Error List when they change the default filter for the "Suppression State" column in the error list to include "Suppressed" diagnostics, see image here. In future, we also plan to add a new error list column, possibly named "Suppression Source", which will display the suppression ID and justification associated with each programmatic suppression.
    3. Disable suppressors: End users can disable specific suppression IDs from an analyzer package using the following two mechanisms:
      1. /nowarn:<%suppression_id%>: See image here for disabling the suppression ID from the project property page, which in turn generates the nowarn command line argument.
      2. Ruleset entry: See image here for disabling the suppression ID using a CodeAnalysis ruleset

Open Questions:

  1. Should the DiagnsoticSuppressor feature be Opt-in OR on-by-default: This was brought up few times in the past, especially with the concern with on-by-default behavior being that an end user would not see any indication in the command line that diagnostic suppressors were executed as part of their build. I think we have following options:

    1. Opt-in:
      1. Keep the feature behind a new, permanent feature flag: This way the command line arguments will always indicate if diagnostic suppressors were involved in the build.
      2. Add a new command line switch to enable suppressors
    2. On-By-default: No command line argument indicating suppressors are involved in a build, but users can look at binlogs and/or verbose msbuild output logs to determine the same.

    The draft PR for this feature currently puts the feature being a feature flag. If we decide to take the on-by-default route, I can revert the changes adding the feature flag. Otherwise, if we decide that we should make it opt-in with a new command line compiler switch, I would like to propose that we keep the feature flag for the initial PR, and then have a follow-up PR to add the command line switch.

    Update: We have decided to keep the feature being a temporary feature flag for initial release, which will be removed once we are confident about the feature's performance, user experience, etc.

  2. Message for the "Info" suppression diagnostic: I have chosen the below message format (with 3 format arguments), but any suggested changes are welcome:
    Diagnostic 'CS0649' was programmatically suppressed by a DiagnosticSuppressor with suppresion ID 'SPR1001' and justification 'Field symbols marked with SerializedFieldAttribute are implicitly assigned by UnityEngine at runtime and hence do not have its default value'

  3. What should be the diagnostic ID for the "Info" suppression diagnostic?: We have following options:

    1. Use a CSxxxx/BCxxxx diagnostic ID for the suppression diagnostic so it is evident that the diagnostic is coming from the core compiler.
    2. Use a distinct diagnostic prefix, such as 'SPR0001', similar to the way we report 'AD0001' diagnostics for analyzer exceptions. This provides a separate bucketing/categorization for these special suppression diagnostics.
      The draft PR chooses the second approach with 'SPR0001' as the diagnostic ID.

    Update: We have decided the diagnostic ID would be SP0001.

Draft PR implementing the above proposal:

https://github.com/dotnet/roslyn/pull/36067

Detailed API proposal with documentation comments from the Draft PR

text
/* Detailed source-code truncated for AI context efficiency. */

Analyzers FixAllProvider (`docs/analyzers/FixAllProvider.md`)

Introduction

This document covers the following:

  • Introductory definitions
  • What is a FixAll occurrences code fix?
  • How is a FixAll occurrences code fix computed?
  • Adding FixAll support to your code fixer
  • Selecting an Equivalence key for code actions
  • Spectrum of FixAll providers
  • Built-in FixAllProvider and its limitations
  • Implementing a custom FixAllProvider

Definitions

  • Analyzer: An instance of a type derived from DiagnosticAnalyzer that reports diagnostics.
  • Code fixer: An instance of a type derived from CodeFixProvider that provides code fixes for compiler and/or analyzer diagnostics.
  • Code refactoring: An instance of a type derived from CodeRefactoringProvider that provides source code refactorings.
  • Code action: An action registered by CodeFixProvider.RegisterCodeFixesAsync that performs a code fix OR an action registered by CodeRefactoringProvider.ComputeRefactoringsAsync that performs a code refactoring.
  • Equivalence Key: A string value representing an equivalence class of all code actions registered by a code fixer or refactoring. Two code actions are treated as equivalent if they have equal EquivalenceKey values and were generated by the same code fixer or refactoring.
  • FixAll provider: An instance of a type derived from FixAllProvider that provides a FixAll occurrences code fix. A FixAll provider is associated with a corresponding code fixer by CodeFixProvider.GetFixAllProvider method.
  • FixAll occurrences code fix: A code action returned by FixAllProvider.GetFixAsync, that fixes all or multiple occurrences of diagnostics fixed by the corresponding code fixer, within a given FixAllScope.

What is a FixAll occurrences code fix?

In layman terms, a FixAll occurrences code fix means: I have a code fix 'C', that fixes a specific instance of diagnostic 'D' in my source and I want to apply this fix to all instances of 'D' across a broader scope, such as a document or a project or the entire solution.

In more technical terms: Given a particular code action registered by a code fixer to fix one or more diagnostics, a corresponding code action registered by its FixAll provider, that applies the original trigger code action across a broader scope (such as a document/project/solution) to fix multiple instances of such diagnostics.

How is a FixAll occurrences code fix computed?

Following steps are used to compute a FixAll occurrences code fix:

  • Given a specific instance of a diagnostic, compute the set of code actions that claim to fix the diagnostic.
  • Select a specific code action from this set. In the Visual Studio IDE, this is done by selecting a specific code action in the light bulb menu.
  • The equivalence key of the selected code action represents the class of code actions that must be applied as part of a FixAll occurrences code fix.
  • Given this code action, get the FixAll provider corresponding to the code fixer that registered this code action.
  • If non-null, then request the FixAll provider for its supported FixAllScopes.
  • Select a specific FixAllScope from this set. In the Visual Studio IDE, this is done by clicking on the scope hyperlink in the preview dialog.
  • Given the trigger diagnostic(s), the equivalence key of the trigger code action, and the FixAll scope, invoke FixAllProvider.GetFixAsync to compute the FixAll occurrences code fix.

Adding FixAll support to your code fixer

Follow the below steps to add FixAll support to your code fixer:

  • Override the CodeFixProvider.GetFixAllProvider method and return a non-null instance of a FixAllProvider. You may either use our built-in FixAllProvider or implement a custom FixAllProvider. See the following sections in this document for determining the correct approach for your fixer.
  • Ensure that all the code actions registered by your code fixer have a non-null equivalence key. See the following section to determine how to select an equivalence key.

Selecting an Equivalence key for code actions

Each unique equivalence key for a code fixer defines a unique equivalence class of code actions. Equivalence key of the trigger code action is part of the FixAllContext and is used to determine the FixAll occurrences code fix.
Normally, you can use the 'title' of the code action as the equivalence key. However, there are cases where you may desire to have different values. Let us take an example to get a better understanding.

Let us consider the C# SimplifyTypeNamesCodeFixProvider that registers multiple code actions and also has FixAll support. This code fixer offers fixes to simplify the following expressions:

  • this expressions of the form 'this.x' to 'x'.
  • Qualified type names of the form 'A.B' to 'B'.
  • Member access expressions of the form 'A.M' to 'M'.

This fixer needs the following semantics for the corresponding FixAll occurrences code fixes:

  • this expression simplification: Fix all should simplify all this expressions, regardless of the member being accessed (this.x, this.y, this.z, etc.).
  • Qualified type name simplification: Fix all should simplify all qualified type names 'A.B' to 'B'. However, we don't want to simplify all qualified type names, such as 'C.D', 'E.F', etc. as that would be too generic a fix, which is not likely intended by the user.
  • Member access expressions: Fix all should simplify all member access expressions 'A.M' to 'M'.

It uses the below equivalence keys for its registered code actions to get the desired FixAll behavior:

  • this expression simplification: Generic resource string "Simplify this expression", which explicitly excludes the contents of the node being simplified.
  • Qualified type name simplification: Formatted resource string "Simplify type name A.B", which explicitly includes the contents of the node being simplified.
  • Member access expressions: Formatted resource string "Simplify type name A.M", which explicitly includes the contents of the node being simplified.

Note that 'this expression simplification' fix requires a different kind of an equivalence class from the other two simplifications. See method GetCodeActionId for the actual implementation.

To summarize, use the equivalence key that best suits the category of fixes to be applied as part of a FixAll operation.

Spectrum of FixAll providers

When multiple fixes need to be applied to documents, there are various ways to do it:

  • Sequential approach: One way to do it is to compute diagnostics, pick one, ask a fixer to produce a code action to fix that, apply it. Now for the resulting new compilation, recompute diagnostics, pick the next one and repeat the process. This approach would be very slow but would lead to correct results (unless it doesn't converge where one fix introduces a diagnostic that was just fixed by a previous fix). We chose to not implement this approach.
  • Batch fix approach: Another way is to compute all the diagnostics, pick each diagnostic and give it to a fixer and apply it to produce a new solution. If there were 'n' diagnostics, there would be 'n' new solutions. Now just merge them all together in one go. This may produce incorrect results (when different fixes change the same region of code in different ways) but it is very fast. We have one implementation of this approach in WellKnownFixAllProviders.BatchFixer
  • Custom approach: Depending on the fix, there may be a custom solution to fix multiple issues. For example, consider an analyzer that simply needs to generate one file as the fix for any instance of the issue. Instead of generating the same file over and over using the previous two approaches, one could write a custom FixAllProvider that simply generates the file once if there were any diagnostics at all.

Since there are various ways of fixing all issues, we've implemented a framework and provided the one general implementation that we think is useful in many cases.

Built-in FixAllProvider

We provide a default BatchFixAllProvider implementation of a FixAll provider that uses the underlying code fixer to compute the FixAll occurrences code fixes.
To use the batch fixer, you should return the static WellKnownFixAllProviders.BatchFixer instance in the CodeFixProvider.GetFixAllProvider override.
NOTE: See the following section on 'Limitations of the BatchFixer' to determine if the batch fixer can be used by your code fixer.

Given a trigger diagnostic, a trigger code action, the underlying code fixer and the FixAll scope, the BatchFixer computes FixAll occurrences code fix with the following steps:

  • Compute all instances of the trigger diagnostic across the FixAll scope.
  • For each computed diagnostic, invoke the underlying code fixer to compute the set of code actions to fix the diagnostic.
  • Collect all the registered code actions that have the same equivalence key as the trigger code action.
  • Apply all these code actions on the original solution snapshot to compute new solution snapshots. The batch fixer only batches code action operations of type ApplyChangesOperation present within the individual code actions, other types of operations are ignored.
  • Sequentially merge the new solution snapshots into a final solution snapshot. Only non-conflicting code actions whose fix spans don't overlap the fix spans of prior merged code actions are retained.

Limitations of the BatchFixer

The BatchFixer is designed for a common category of fixers where fix spans for diagnostics don't overlap with each other. For example, assume there is a diagnostic that spans a particular expression, and a fixer that fixes that expression. If all the instances of this diagnostic are guaranteed to have non-overlapping spans, then their fixes can be computed independently and this batch of fixes can be subsequently merged together.

However, there are cases where the BatchFixer might not work for your fixer. Following are some such examples:

  • Code fixer registers code actions without an equivalence key or with a null equivalence key.
  • Code fixer registers non-local code actions, i.e. a code action whose fix span is completely distinct from diagnostic span. For example, a fix that adds a new declaration node. Multiple such fixes are likely to have overlapping spans and hence could be conflicting.
  • Diagnostics to be fixed as part of FixAll occurrences have overlapping spans. It is likely that fixes for such diagnostics will have overlapping spans too, and hence would conflict with each other.
  • Code fixer registers code actions with operations other than ApplyChangesOperation. BatchFixer ignores such operations and hence may produce unexpected results.

Implementing a custom FixAllProvider

For cases where you cannot use the BatchFixer, you must implement your own FixAllProvider. It is recommended that you create a singleton instance of the FixAll provider, instead of creating a new instance for every CodeFixProvider.GetFixAllProvider invocation.
Following guidelines should help in the implementation:

  • GetFixAsync: Primary method to compute the FixAll occurrences code fix for a given FixAllContext. You may use the set of 'GetXXXDiagnosticsAsync' methods on the FixAllContext to compute the diagnostics to be fixed. You must return a single code action that fixes all the diagnostics in the given FixAll scope.
  • GetSupportedFixAllScopes: Virtual method to get all the supported FixAll scopes. By default, it returns all the three supported scopes: document, project and solution scopes. Generally, you need not override this method. However, you may do so if you wish to support a subset of these scopes.
  • GetSupportedFixAllDiagnosticIds: Virtual method to get all the fixable diagnostic ids. By default, it returns the underlying code fixer's FixableDiagnosticIds. Generally, you need not override this method. However, you may do so if you wish to support FixAll only for a subset of these ids.

See DeclarePublicAPIFix for an example implementation of a custom FixAllProvider.


Analyzers Localizing Analyzers (`docs/analyzers/Localizing Analyzers.md`)

Introduction

This document covers the following:

  • Setting up your project to use localized resources
  • Using localized resources in Analyzers
  • Using localized resources in Code Fixes
  • Producing localized resource assemblies
  • Packaging resource assemblies in NuGet packages and VSIXes
  • Testing the localization at the command line and in VS

Setting up the Project

You'll need to create a .resx file to hold your localizable resources. For the purposes of this document, we'll assume you are putting them in a file named 'AnalyzerResources.resx'.

Using Localized Resources in Analyzers

Normally, .NET applications can simply access the resource through the type generated from the .resx file:

c#
string message = AnalyzerResources.Message;

and the framework will use the culture specified in System.Globalization.CultureInfo.CurrentUICulture to automatically find and return the appropriately localized resource.

However, the C# and VB compilers provide the /preferreduilang switch to specify the use of resources for a different culture from that in CurrentUICulture. Because of that accessing AnalyzerResources.Message directly within an analyzer will not return the correct resource.

Instead you need to access these resources through the LocalizableResourceString type. For example:

c#
LocalizableResourceString message = new LocalizableResourceString(nameof(AnalyzerResources.Message), AnalyzerResources.ResourceManager, typeof(AnalyzerResources));

Using Localized Resources in Code Fixes

Code Fixes, on the other hand, are not affected by the /preferreduilang switch. Resources in Code Fixes should be accessed in the standard way:

c#
string codeFixMessage = AnalyzerResources.CodeFixMessage;

Producing Localized Resource Assemblies

To create resource assemblies, you simply need to add additional .resx files with the desired language name in the file name. For example, if you wanted to create a resource assembly with German strings, you would add AnalyzerResources.de.resx. For Japanese, you add AnalyzerResources.ja.resx, and so on.

Inside these files you add items where the names match those in AnalyzerResources.resx and the values have the localized content. If AnalyzerResources.resx contains the following:

Name Value
Message Hello!

Then AnalyzerResources.de.resx should contain this:

Name Value
Message Guten Tag!

Now when you build resource assemblies are automatically produced in language-specific sub folders:

  • bin\Debug\
    • MyAnalyzer.dll
    • MyAnalyzer.pdb
    • de\
      • MyAnalyzer.resources.dll
    • ja\
      • MyAnalyzer.resources.dll

This layout is also what the CLR expects at runtime: for a given assembly Foo.dll, the localized resources should be in language-specific folders next to the assembly.

Packaging Resource Assemblies

NuGet

NuGet packaging is straight-forward, as you simply need to duplicate the structure of the build output:

xml
<files>
  <file src="MyAnalyzer.dll" target="tools\analyzers" />
  <file src="de\MyAnalyzer.resources.dll" target="tools\analyzers\de" />
  <file src="ja\MyAnalyzer.resources.dll" target="tools\analyzers\ja" />
  ...
</files>

VSIX

Extension projects automatically incorporate satellite assemblies into the resulting .vsix file.

Testing Analyzer Localization

At the command line

Once the NuGet package is installed, you can force the build to use localized resources by passing the /preferreduilang flag to csc.exe or vbc.exe:

text
csc.exe /preferreduilang:de /t:library /analyzer:... Foo.cs

Alternatively, you can pass it to MSBuild as a property:

text
msbuild /p:PreferredUILang=de

or set it as a property in the *proj file itself:

text
de

In VS

In VS analyzers use the selected UI language. This can be changed by selecting "Options..." from the "Tools" menu, and navigating to Environment -> International Settings.


Analyzers Report Analyzer Format (`docs/analyzers/Report Analyzer Format.md`)

Introduction

The C# and Visual Basic compilers support a /reportanalyzer switch on
the command line to report additional analyzer information, such as execution time.

The output contains the total wall clock time spent in executing the analyzers and
the relative execution times per-analyzer.
Note that the elapsed time may be less than analyzer execution time because
analyzers can run concurrently. One should use this data only for comparative analysis to
identify any outlier analyzer which takes signinficantly more time than other analyzers.

MSBuild command

Use the following msbuild command line to get the analyzer performance report:

text
msbuild.exe /v:d /p:reportanalyzer=true <%project_file%>

Output Format

text
Total analyzer execution time: XYZ seconds.
NOTE: Elapsed time may be less than analyzer execution time because analyzers can run concurrently.
Time (s) % Analyzer
xyz1 X Analyzer Assembly Identity
xyz2 Y - DiagnosticAnalyzer1
xyz3 Z - DiagnosticAnalyzer2

Example

Following is an example output from a project built with analyzers:


text
/* Detailed source-code truncated for AI context efficiency. */

Analyzers Using Additional Files (`docs/analyzers/Using Additional Files.md`)

Introduction

This document covers the following:

  • Uses of Additional Files
  • Passing Additional Files on the command line
  • Specifying an individual item as an AdditionalFile in an MSBuild project
  • Specifying an entire set of items as AdditionalFiles in an MSBuild project
  • Accessing and reading additional files through the AnalyzerOptions type
  • Includes sample analyzers that read additional files

Uses

Sometimes an analyzer needs access to information that is not available through normal compiler inputs--source files, references, and options. To support these scenarios the C# and Visual Basic compilers can accept additional, non-source text files as inputs.

For example, an analyzer may enforce that a set of banned terms is not used within a project, or that every source file has a certain copyright header. The terms or copyright header could be passed to the analyzer as an additional file, rather than being hard-coded in the analyzer itself.

On the Command Line

On the command line, additional files can be passed using the /additionalfile option. For example:

text
csc.exe alpha.cs /additionalfile:terms.txt

In a Project File

Passing an Individual File

To specify an individual project item as an additional file, set the item type to AdditionalFiles:

xml
<ItemGroup>
  <AdditionalFiles Include="terms.txt" />
</ItemGroup>

Passing a Group of Files

Sometimes it isn't possible to change the item type, or a whole set of items need to be passed as additional files. In this situation you can update the AdditionalFileItemNames property to specify which item types to include. For example, if your analyzer needs access to all .resx files in the project, you can do the following:

xml
<PropertyGroup>
  
  <AdditionalFileItemNames>$(AdditionalFileItemNames);EmbeddedResource</AdditionalFileItemNames>
</PropertyGroup>
<ItemGroup>
  
  <EmbeddedResource Include="Terms.resx">
    ...
  </EmbeddedResource>
</ItemGroup>

Accessing Additional Files

The set of additional files can be accessed via the Options property of the context object passed to a diagnostic action. For example:

c#
CompilationAnalysisContext context = ...;
ImmutableArray<AdditionalText> additionFiles = context.Options.AdditionalFiles;

From an AdditionalText instance, you can access the path to the file or the contents as a SourceText:

c#
AdditionText additionalFile = ...;
string path = additionalFile.Path;
SourceText contents = additionalFile.GetText();

Samples

Reading a File Line-by-Line

This sample reads a simple text file for a set of terms--one per line--that should not be used in type names.

text
/* Detailed source-code truncated for AI context efficiency. */

Converting a File to a Stream

In cases where an additional file contains structured data (e.g., XML or JSON) the line-by-line access provided by the SourceText may not be desirable. One alternative is to convert a SourceText to a string, by calling ToString() on it. This sample demonstrates another alternative: converting a SourceText to a Stream for consumption by other libraries. The terms file is assumed to have the following format:

xml
<Terms>
  <Term>frob</Term>
  <Term>wizbang</Term>
  <Term>orange</Term>
</Terms>
text
/* Detailed source-code truncated for AI context efficiency. */

Compilers Boolean Representation (`docs/compilers/Boolean Representation.md`)

Representation of Boolean Values

The C# and VB compilers represent true (True) and false (False) bool (Boolean) values with the single byte values 1 and 0, respectively, and assume that any boolean values that they are working with are restricted to being represented by these two underlying values. The ECMA 335 CLI specification permits a "true" boolean value to be represented by any nonzero value. If you use boolean values that have an underlying representation other than 0 or 1, you can get unexpected results. This can occur in unsafe code in C#, or by interoperating with a language that permits other values. To avoid these unexpected results, it is the programmer's responsibility to normalize such incoming values.

See also https://github.com/dotnet/roslyn/issues/24652


Compilers Co Located Core Types (`docs/compilers/Co-located core types.md`)

Co-located core types

Customer Scenario and Context

The ECMA 335 spec indicates that a few types, such as System.Object, or System.Int16, can be referenced in metadata as an integer value rather than a type ref token (built-in types). Additionally, the spec indicates that certain conversions are allowed between these types and a small set of token-referenced types – we shall refer to these as special types. Finally, a few types have special runtime treatment without being referenced like the other built-in types.

Scenarios

  1. A compiler needs to find the type definition for a built-in type, given a reference to it. Given that the reference is not a type token, the compiler needs to know the assembly containing the def

  2. A compiler encounters two definitions for a built-in type

  3. A compiler is asked to type check two references to a built-in type, one of which is originating from a library compiled to an incompatible definition of core types

Design

Compilers may assume that there is exactly one assembly representing the "core assembly". This core assembly is either referenced or is currently being compiled. The core assembly must not reference any other assemblies.

The core assembly contains the type definitions for the subset of built-in and special types supported on a platform.

References to types having the same name as built-in types but found outside the core assembly are made via type tokens.

An assembly A referencing only built-in types may exclude (as per ECMA spec) a reference to the core library. In the case A is further referenced by the compiler when compiling a second assembly B, the references to the built-in types in A are resolved by the compiler based on the core library used for compiling B.

A platform implementation may choose to define a core assembly with only a subset of the built-in and special types. Compilation in such a scenario succeeds, as long as built-in or special types outside of the subset are not referenced, directly (by user code) or indirectly (by compiler features). This means that, while the compiler can safely assume that all built-in and special types are co-located in the same assembly, they may not all be present.

If a later version of the platform introduces more built-in or special types, they must do so in the core assembly.

Correspondingly, a runtime for such a platform need only support the subset in the corresponding core assembly.

The list of co-located types is exactly the following:

  • System.Object
  • System.Enum
  • System.MulticastDelegate
  • System.Delegate
  • System.ValueType
  • System.Void
  • System.Boolean
  • System.Char
  • System.SByte
  • System.Byte
  • System.Int16
  • System.UInt16
  • System.Int32
  • System.UInt32
  • System.Int64
  • System.UInt64
  • System.Decimal
  • System.Single
  • System.Double
  • System.String
  • System.IntPtr
  • System.UIntPtr
  • System.Array
  • System.DateTime
  • System.Collections.IEnumerable
  • System.Collections.Generic.IEnumerable<>
  • System.Collections.Generic.IList<>
  • System.Collections.Generic.ICollection<>
  • System.Collections.Generic.IEnumerator<>
  • System.Collections.IEnumerator
  • System.Nullable<>
  • System.Runtime.CompilerServices.IsVolatile
  • System.IDisposable
  • System.TypedReference
  • System.IAsyncResult
  • System.AsyncCallback
  • System.Collections.Generic.IReadOnlyList<>
  • System.Collections.Generic.IReadOnlyCollection<>

We continue to assume that references to System.Type are made by-enumeration value when the reference is made in a custom attribute, and as a regular reference when not. Note the departure from the ECMA spec which included System.Type. System.Type and other reflection types are intentionally not in the core library. Since System.Type may be referenced as a built-in type, the compiler may assume that there is at most one definition for this type available in the compilation context, and treat as an error the case where there is more than one. No definition should be accepted as long as there is no reference.

Language features relying on APIs not in the list should not assume any particular assembly location for the definition of such APIs. Compilers should either use heuristics, or require explicit specification of the location of such APIs.


Compilers Compiler Server (`docs/compilers/Compiler Server.md`)

Compiler Server

As a performance optimization, the Roslyn C# and VB compilers support using a
server process to execute compilations instead of executing the compilation
in-process. Engineering has focused on three performance benefits from the server:

  1. Re-using a single process to do compilation amortizes the cost of JITing the
    compiler code. This is usually the single largest performance benefit.
  2. Roslyn requires assembly references to be hydrated into a rich object
    model to perform compilation. The compiler server holds a fixed-size cache of
    metadata symbols, which can help compilation times if most compilations share
    a sizable portion of metadata references (e.g., mscorlib).
  3. On Windows process creation itself is expensive, so dispatching compilations
    directly from MSBuild saves creations of csc.exe.

Usage

The primary way to use the compiler server is via the
Microsoft.Build.Tasks.CodeAnalysis MSBuild task, which ships in both the
dotnet SDK and Visual Studio. The build task contains a build client which,
rather than executing the csc.exe or vbc.exe, creates the
VBCSCompiler.exe process and dispatches the command line arguments to the
server directly.

If you're not using MSBuild, there's also a flag, /shared which can be
passed directly to csc.exe, which causes csc.exe to behave as a client
and dispatches the actual compilation to the server process. In this mode
csc.exe doesn't perform any computation itself, it just dispatches the
command line to the server process (starting one if one does not already
exist), and then reports the results of the compilation (including any
warnings/errors produced).

Importantly, the server is a performance optimization. If server compilation
fails for any reason, the compiler will fall back to stand-alone compilation.
For MSBuild, this means creating a standard csc.exe process. For /shared
this means continuing the compilation in-process, as though /shared were
not passed. No diagnostics are produced in this case, as there are very
common environmental reasons for any given server compilation to fail.

This also implies that it is always safe to kill the compiler server process,
as the client will always recover gracefully.

Architecture

Roslyn supports a client-server protocol where multiple clients can dispatch
to multiple servers and multiple servers can run concurrently on the machine.
The client and server must run on the same machine, as the client and server
only exchange a command line, not real assets.

Troubleshooting

To diagnose problems with the server compilation, you can enable an
environment variable that writes a diagnostic log file:

text
(UNIX): export RoslynCommandLineLogFile=/path/to/log/file
(Windows CMD, using directory path): setx RoslynCommandLineLogFile="C:\path\to\log"
(Windows CMD, using file path): setx RoslynCommandLineLogFile="C:\path\to\log\file.log"

This file contains diagnostic logging for both the client and server,
which you can distinguish using the PID tag for each diagnostic line.
Sample output should look something like this:

text
--- PID=90433 TID=16 Ticks=1048119217: Attempt to open named pipe 'angocke.F.MW9E3dnrZddlsgI8MNFfpJyup'
--- PID=90433 TID=16 Ticks=1048119217: Attempt to connect named pipe 'angocke.F.MW9E3dnrZddlsgI8MNFfpJyup'
--- PID=90436 TID=1 Ticks=244465170: Keep alive timeout is: 600000 milliseconds.
--- PID=90436 TID=1 Ticks=244465182: Constructing pipe 'angocke.F.MW9E3dnrZddlsgI8MNFfpJyup'.
--- PID=90436 TID=1 Ticks=244465182: Successfully constructed pipe 'angocke.F.MW9E3dnrZddlsgI8MNFfpJyup'.

Here PID 90433 is the client, as it's attempting to open a connection
(named pipe), while PID 90436 is the server, as it is constructing the
named client. If it succeeds, the result looks something like:

text
--- PID=90436 TID=8 Ticks=244476082: ****C# Compilation complete.
****Return code: 0
****Output:
Microsoft (R) Visual C# Compiler version 2.9.0.63018 (e3fa7d6a)
Copyright (C) Microsoft Corporation. All rights reserved.

Otherwise, you may be able to discover what's going wrong from other output,
like exception messages.


Compilers Compiler Toolset NuPkgs (`docs/compilers/Compiler Toolset NuPkgs.md`)

Compiler Toolset NuPkgs

Summary

The compiler produces the Microsoft.Net.Compilers.Toolset NuPkg
from all of Roslyn's main branches. When this NuPkg is installed it will
override the compiler that comes with MSBuild with the version from the branch
it was built in.

This package is meant to support the following scenarios:

  1. Allows compiler team to provide rapid hot fixes to customers who hit a blocking
    issue. This package can be installed until the fix is available in .NET SDK or
    Visual Studio servicing.
  2. Serves as a transport mechanism for the Roslyn binaries in the greater .NET
    SDK build process.
  3. Allows customers to conduct experiments on various Roslyn builds, many of
    which aren't in an official shipping product yet.

This package is not meant to support using newer compiler versions in an
older version of MSBuild. For example using Microsoft.Net.Compilers.Toolset
3.5 (C# 8) inside MSBuild 15 is explicitly not a supported scenario.

Customers who want to use the compiler as a part of their supported build
infrastructure should use the Visual Studio Build Tools SKU
) or .NET SDK

NuPkg Installation

To install the NuPgk run the following:

cmd
> nuget install Microsoft.Net.Compilers.Toolset   # Install C# and VB compilers

Daily NuGet builds of the project are also available in our Azure DevOps feed:

https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-tools/nuget/v3/index.json

Microsoft.Net.Compilers

The Microsoft.Net.Compilers
NuPkg is deprecated. It is a .NET Desktop specific version of
Microsoft.Net.Compilers.Toolset and will not be produced anymore after the
3.6.0 release. The Microsoft.Net.Compilers.Toolset package is a drop in
replacement for it for all supported scenarios.


Compilers Deterministic Inputs (`docs/compilers/Deterministic Inputs.md`)

Deterministic Inputs

The C# and VB compilers are fully deterministic when the /deterministic option is specified (this is the default in the .NET SDK). This means that the "same inputs" will cause the compilers to produce the "same outputs" byte for byte.

The following are considered inputs to the compiler for the purpose of determinism:

  • The sequence of command-line parameters (order is important)
  • The precise version of the compiler used and the files included in its deployment: reference assemblies, rsp, etc ...
  • Current full directory path (you can reduce this to a relative path; see https://github.com/dotnet/roslyn/issues/949)
  • (Binary) contents of all files explicitly passed to the compiler, directly or indirectly, including
    • source files
    • referenced assemblies
    • referenced modules
    • resources
    • the strong name key file
    • @ response files
    • Analyzers
    • Generators
    • Rulesets
    • "additional files" that may be used by analyzers and generators
  • The current culture if /preferreduilang is not specified (for the language in which diagnostics and exception messages are produced).
  • The current OS code page if /codepage is not specified and any of the input source files do not have BOM and are not UTF-8 encoded.
  • The existence, non-existence, and contents of files on the compiler's search paths (specified, e.g. by /lib or /recurse)
  • The CLR platform on which the compiler is run:
    • The result of double arithmetic performed for constant-folding may use excess precision on some platforms.
    • The compiler uses Unicode tables provided by the platform.
  • The version of the zlib library that the CLR uses to implement compression (when /embed or /debug:embedded is used).
  • The value of %LIBPATH%, as it can affect reference discovery if not fully qualified and how the runtime handles analyzer / generator dependency loading.
  • 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 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

Compilers Error Log Format (`docs/compilers/Error Log Format.md`)

The C# and Visual Basic compilers support a /errorlog: switch on
the command line to log all diagnostics in a structured, JSON format.

The log format is SARIF (Static Analysis Results Interchange Format):
See https://sarifweb.azurewebsites.net/ for the format specification,
JSON schema, and other related resources.

SARIF v2 format

This section provides a high level overview of the contents of the SARIF v2 error log file generated by the C# and Visual Basic compilers. A more formal and detailed schema specification for the SARIF v2 format is specified at http://json.schemastore.org/sarif-2.1.0.

  1. schema and version information: The first two lines of the error log file specifies the SARIF schema and version information. For example:
json
"$schema": "http://json.schemastore.org/sarif-2.1.0",
"version": "2.1.0",
  1. runs information: The core entry in the error log file is the runs section with a single run entry within it for the build. The run entry has the following main parts:
    1. results section: This section contains an array of result entries, where each result corresponds to information about a reported compiler or analyzer Diagnostic. More details in Result format for each compiler or analyzer Diagnostic instance.
    2. properties section: This section contains custom key-value properties associated with the run. Following properties are currently emitted:
      1. analyzerExecutionTime: Execution time in seconds for execution of all analyzers, as reported with the /reportAnalyzer compiler switch.
    3. tools section: This section contains information about the compiler build and version. Additionally, it contains a rules array, where each rule entry corresponds to metadata or DiagnosticDescriptor information about each reported analyzer diagnostic. More details in Rule format for each analyzer supported DiagnosticDescriptor instance
    4. invocations section: This section contains information about the end user specified configurations for the rules for compiler invocation/run. We emit this section only if at least one rule had one or more severity configuration overrides for a part or whole of the compilation via options. More details in Rule Configuration Override format for each rule severity override.
    5. columnKind section: This section contains information about the unit in which the tool measures columns. C# and Visual Basic compilers uses utf16 code units.

Example runs section, with stripped off results, rules and ruleConfigurationOverrides sections:

json
"runs": [
{
  "results": [
  ],
  "properties": {
    "analyzerExecutionTime": "x.xxx"
  },
  "tool": {
    "driver": {
      "name": "Microsoft (R) Visual C# Compiler",
      "version": "4.4.0-dev (<developer build>)",
      "dottedQuadFileVersion": "42.42.42.42",
      "semanticVersion": "42.42.42",
      "language": "en-US",
      "rules": [
      ]
    }
  },
  "invocations": [
    {
      "executionSuccessful": true,
      "ruleConfigurationOverrides": [
      ]
    }
  ],
  "columnKind": "utf16CodeUnits"
}
]

`Result` format for each compiler or analyzer `Diagnostic` instance

The results section contains an array of result entries, where each result corresponds to information about a reported compiler or analyzer Diagnostic. It contains the below data:

  1. ruleId: Rule ID or diagnostic ID for the diagnostic.
  2. ruleIndex: Index into the rules section for the underlying DiagnosticDescriptor for the diagnostic. See Rule format for each analyzer supported DiagnosticDescriptor instance for more details.
  3. level: Severity level for the diagnostic, such as error, warning, note, etc.
  4. message: User facing message for the diagnostic.
  5. suppressions: For each diagnostic instance that is suppressed with a pragma directive, SuppressMessageAttribute or via a DiagnosticSuppressor, the result entry contains suppressions section with the following data:
    1. kind information: C# and Visual Basic compilers support a single inSource suppression kind.
    2. justification information: Justification pertaining to the suppression. Currently, this field is populated only for SuppressMessageAttribute based suppression, with a non-null justification argument.
    3. suppressionType property with one of these three values: Pramga Directive, SuppressMessageAttribute, or DiagnosticSuppressor. This data helps analyze the preferred in-source suppression mechanisms in a code base.
  6. locations: One or more locations associated with the diagnostic.
  7. properties: One or more custom key-value string pairs associated with the diagnostic.

Example result entry:

json
{
  "ruleId": "CA1822",
  "ruleIndex": 97,
  "level": "warning",
  "message": {
    "text": "Member 'M2' does not access instance data and can be marked as static"
  },
  "suppressions": [
  {
    "kind": "inSource",
    "justification": "<Pending>",
    "properties": {
      "suppressionType": "SuppressMessageAttribute"
    }
  }
  ],
  "locations": [
  {
    "physicalLocation": {
      "artifactLocation": {
        "uri": "file:///C:/source/repos/ClassLibrary1/Class1.cs"
      },
      "region": {
        "startLine": 13,
        "startColumn": 10,
        "endLine": 13,
        "endColumn": 12
      }
    }
  }
  ],
  "properties": {
    "warningLevel": 1
  }
}

`Rule` format for each analyzer supported `DiagnosticDescriptor` instance

The rules section contains a rule entry that corresponds to metadata or DiagnosticDescriptor information about supported descriptors for each analyzer. We report the DiagnosticDescriptor info for all analyzers that were provided to the compilation, regardless of whether or not they executed on the entire compilation, part of the compilation or were disabled for the entire compilation. A rule entry contains the below data:

  1. id: Rule ID or Diagnostic ID associated with the descriptor.
  2. shortDescription: User facing short description or Title associated with the descriptor.
  3. fullDescription: User facing full description or Description associated with the descriptor.
  4. defaultConfiguration: Default severity level for the diagnostics reported for the descriptor, such as error, warning, note, etc.
  5. helpUri: Help uri for help information associated with the descriptor.
  6. properties: One or more custom properties associated with the descriptor. It includes the following:
    1. category: Category associated with the descriptor, such as, Design, Performance, Security, etc.
    2. isEverSuppressed and suppressionKinds: If a rule had either a source suppression or was disabled for part or whole of the compilation via options, the rule metadata contains a special flag isEverSuppressed = true and an array suppressionKinds with either or both of the below suppression kinds:
      1. inSource suppression kind for one or more reported diagnostic(s) that were suppressed through pragma directive, SuppressMessageAttribute or a DiagnosticSuppressor.
      2. external suppression kind for diagnostic ID that is disabled either for the entire compilation (via global options such as /nowarn, ruleset, globalconfig, etc.) or for certain files or folders in the compilation (via editorconfig options).
    3. executionTimeInSeconds: Execution time in seconds associated with the analyzer, as reported with the /reportAnalyzer compiler switch. Values less than 0.001 seconds are reported as <0.001.
    4. executionTimeInPercentage: Execution time in percentage associated with the analyzer, as reported with the /reportAnalyzer compiler switch. Values less than 1 are reported as <1.
    5. tags: An array of one of more CustomTags associated with the descriptor.

Example rule entry:

json
{
  "id": "CA1001",
  "shortDescription": {
    "text": "Types that own disposable fields should be disposable"
  },
  "fullDescription": {
    "text": "A class declares and implements an instance field that is a System.IDisposable type, and the class does not implement IDisposable. A class that declares an IDisposable field indirectly owns an unmanaged resource and should implement the IDisposable interface."
  },
  "defaultConfiguration": {
    "level": "note"
  },
  "helpUri": "https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1001",
  "properties": {
    "category": "Design",
    "isEverSuppressed": "true",
    "suppressionKinds": [
      "external"
    ],
    "executionTimeInSeconds": "x.xxx",
    "executionTimeInPercentage": "xx",
    "tags": [
      "PortedFromFxCop",
      "Telemetry",
      "EnabledRuleInAggressiveMode"
    ]
  }
}

`Rule Configuration Override` format for each rule severity override

The ruleConfigurationOverrides section contains an array of entries, with an entry for every rule severity configuration override for the whole or part of the compilation via options, i.e. editorconfig, globalconfig, command line options, ruleset, etc. Each rule configuration override entry contains the following information:

  1. descriptor: Property containing information to map back to the relevant descriptor or the rule in the rules section. Currently, it contains the below child properties:
    1. id: Rule id string for the rule or diagnostic.
    2. index: A zero-based integral index value into the rules array for the rule mapping.
  2. configuration: Configuration property for the effective overridden severity for the rule. Currently, it may include one of the below child properties:
    1. enabled: A boolean property indicating if the rule has been explicitly disabled or enabled with the configuration override entry.
    2. level: Property for the effective severity of the rule, such as error, warning or note. Note that this property is not emitted for rules disabled with configuration override, we instead emit enabled: false for them.

Note that we can have multiple ruleConfigurationOverride entries for the same descriptor if the rule is configured to fire at different severities across different parts of the compilation. For example, editorconfig based severity configurations can specify different effective severity for the same rule ID for different files or folders in a project.

Example ruleConfigurationOverride entry:

json
{
  "descriptor": {
    "id": "CA1000",
    "index": 0
  },
  "configuration": {
    "level": "error"
  }
},
{
  "descriptor": {
    "id": "CA1000",
    "index": 0
  },
  "configuration": {
    "enabled": false
  }
},
{
  "descriptor": {
    "id": "CA1001",
    "index": 1
  },
  "configuration": {
    "level": "warning"
  }
}

Compilers Extension Methods In Aliased Assemblies (`docs/compilers/Extension Methods in Aliased Assemblies.md`)

Extension Methods in the Global Namespace

If you create a compilation in which an imported library is assigned an alias (instead of the default "global" alias), the Roslyn compilers do not automatically see extension methods in the (unnamed top-level) global namespace in that library. You would have to import the library in your code using the alias for these methods to be treated as extension methods.

Previous versions of the compiler would treat such methods as extension methods despite not being imported.

See also https://github.com/dotnet/roslyn/issues/1079.


Compilers IOperation Test Hook (`docs/compilers/IOperation Test Hook.md`)

IOperation Test Hook

The compiler has a test hook designed to ensure that IOperation nodes and scenarios match up with information that can be retrieved from the semantic model,
and to help ensure that we have code coverage that ensures IOperation will not fail or violate invariants for scenarios we don't have direct testing on. This
works by running a hook whenever the test framework creates a Compilation object: for every syntax tree in the compilation, we enumerate every syntax node
and verify that GetOperation does not crash, and returns information that matches up with the SemanticModel (ie, GetTypeInfo matches the IOperation type,
for example). We also fetch and verify the control flow graph for every member body in the compilation and run the CFG verifier to ensure that all invariants
are presevered.

Feature Development

For feature development, this means that as we shift around BoundNodes, introduce new nodes, or enable code that didn't exist before, the hook may hit failures
because the IOperation machinery doesn't handle that particular node, or had logic that now triggers a Debug.Assert because the new code violates some pre-existing
assumption of the factories. For fixing these issues, there are a couple of options:

  1. Implement the IOperation changes in the same PR. This is the only acceptable way to do this in a production branch.
  2. Add the new BoundKind to the catch-all switch case at the bottom of CSharpOperationFactory/VisualBasicOperationFactory.Create, with a prototype comment
    to ensure that support is implemented before the feature branch is merged back to a production branch. Sometimes, the test hits other failure spots, such as
    SemanticModel failures or mismatches. For these, the test can be marked with ConditionalFact(typeof(NoIOperationValidation)) to skip the test in the test
    hook. When doing so, please mark with a prototype comment (either as a comment above or as the reason string for ConditionalFactAttribute) so that it is fixed
    before merging to production.

Replicating Failures

In order to replicate test failures, there are 2 options:

  1. Uncomment src/Compilers/Test/Core/Compilation/CompilationExtensions.cs:7, which defines ROSLYN_TEST_IOPERATION, and run your tests. Do not check this in, as it
    will enable the test hook for every test in every project and significantly slow down regular test runs.
  2. Set the ROSLYN_TEST_IOPERATION environment variable and restart VS with it set.
  3. Set a breakpoint at the start of ValidateIOperations, in src/Compilers/Test/Core/Compilation/CompilationExtensions.cs. When it breaks, use VS's jump to location or
    drag the instruction pointer past the early check and return on EnableVerifyIOperation, which will run the code for the current test run.

In either case, after this has been done all tests will run the hook as part of running the test, and you can set breakpoints as normal. It is often helpful to
run until an exception has been hit and then see what nodes were being processed, as most tests have multiple methods and understanding the specific context is
useful for figuring out the earlier steps that may have gone wrong.

When a test failure is isolated, please add a dedicated IOperation test for this to make it easier to avoid future regressions. Preferrably, don't replicate
the entire original test, just enough to hit the bug to ensure that it's protected against regressions.


Compilers Interactive Imports (`docs/compilers/Interactive Imports.md`)

Imports in Scripts/Interactive

Guiding principles

Files includes by #load are each in a separate lexical scope.
Declarations (including aliases) take precedence over usings (including using static) because they express intent.

Process

Steps (top to bottom) Binding Using Binding Code #load
Look for declarations in the current submission/file (including aliases) Yes, excluding aliases Yes, in submission Yes, in file
Look for declarations in preceding submissions (including aliases), in order Yes Yes No
Look for members in the host object (if any) Yes Yes Yes
Look for declarations in the global namespace Yes Yes Yes
Look in usings (except aliases) in the current submission/file and all preceding submissions (all at once, not looping) No Yes, in all submissions Yes, in file only
Look in the global imports of the current submission and all preceding submissions (all at once, not looping) No Yes Yes

Compilers Rule Set Format (`docs/compilers/Rule Set Format.md`)

Introduction

This document, in conjunction with the Rule Set Schema, describes the structure of .ruleset files used by the C# and Visual Basic compilers to turn diagnostic analyzers on and off and to control their severity.

This document only discusses the required and common parts of .ruleset files; for a full set, please see the schema file.

Sample

The following demonstrates a small but complete example of a .ruleset file.

xml
<RuleSet Name="Project WizBang Rules"
         ToolsVersion="12.0">
  <Include Path="..\OtherRules.ruleset" 
           Action="Default" />
  <Rules AnalyzerId="System.Runtime.Analyzers"
         RuleNamespace="System.Runtime.Analyzers">
    <Rule Id="CA1027" Action="Warning" />
    <Rule Id="CA1309" Action="Error" />
    <Rule Id="CA2217" Action="Warning" />
  </Rules>
  <Rules AnalyzerId="System.Runtime.InteropServices.Analyzers"
         RuleNamespace="System.Runtime.InteropService.Analyzers">
    <Rule Id="CA1401" Action="None" />
    <Rule Id="CA2101" Action="Error" />
  </Rules>
</RuleSet>

Passing Rule Sets to the Compiler

Command Line

Rule set files can be passed to csc.exe and vbc.exe using the /ruleset switch, which takes an absolute or relative file path. For example:

text
vbc.exe /ruleset:ProjectRules.ruleset ...
vbc.exe /ruleset:..\..\..\SolutionRules.ruleset ...
vbc.exe /ruleset:D:\WizbangCorp\Development\CompanyRules.ruleset ...

MSBuild Projects

Within MSBuild project files the rule set can be specified via the CodeAnalysisRuleSet property. For example:

xml
<PropertyGroup>
  <CodeAnalysisRuleSet>ProjectRules.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>

Note that because the rule set is specified via a property rather than an item, IDEs like Visual Studio will not show the rule set as a file in your project by default. For this reason it is common to explicitly include the file as an item as well:

xml
<ItemGroup>
  <None Include="ProjectRules.ruleset" />
</ItemGroup>

Elements

`RuleSet`

The RuleSet is the root element of the file.

Attributes:

  • Name (required): A short, descriptive name for the file.
  • Description (optional): A longer description of the purpose of the rule set file.
  • ToolsVersion (required): This attribute is required for backwards compatibility with other tools that use .ruleset files. In practice it is the version of Visual Studio that produced this .ruleset file. When in doubt, simply use "12.0".

Children: Include, Rules

`Include`

Pulls in the settings from the specified rule set file. Settings in the current file override settings in the included file.

Parent: RuleSet

Attributes:

  • Path (required): An absolute or relative path to another .ruleset file.
  • Action (required): Specifies the effective action of the included rules. Must be one of the following values:
  • Default - The rules use the actions specified in the included file.
  • Error - The included rules are treated as though their action values were all "Error".
  • Warning - The included rules are treated as though their actions values were all "Warning".
  • Info - The included rules are treated as though their actions values were all "Info".
  • Hidden - The included rules are treated as though their actions values were all "Hidden".
  • None - The included rules are treated as though their actions values were all "None".

Children: None.

`Rules`

Hold settings for rules from a single diagnostic analyzer.

Parent: RuleSet

Attributes:

  • AnalyzerId (required): The name of the analyzer. In practice this is the simple name of the assembly containing the diagnostics.
  • RuleNamespace (required): This attribute is required for backwards compatibility with other tools that use .ruleset files. In practice it is generally the same as the AnalyzerId.

Children: Rule

`Rule`

Specifies what action to take for a given diagnostic rule.

Parent: Rules

Attributes:

  • Id (required): The ID of the diagnostic rule.
  • Action (required): One of the following values:
  • Error - Instances of this diagnostic are treated as compiler errors.
  • Warning - Instances of this diagnostic are treated as compiler warnings.
  • Info - Instances of this diagnostic are treated as compiler messages.
  • Hidden - Instances of this diagnostic are hidden from the user.
  • None - Turns off the diagnostic rule.

Children: None.

Notes:

Within a Rules element, a Rule with a given Id may only appear once. The C# and Visual Basic compilers impose a further constraint: if multiple Rules elements contain a Rule with the same Id they must all specify the same Action value.

The difference between the Hidden and None actions is subtle but important. When a diagnostic rule is set to Hidden instances of that rule are still created, but aren't displayed to the user by default. However, the host process can access these diagnostics and take some host-specific action. For example, Visual Studio uses a combination of Hidden diagnostics and a custom editor extension to "grey out" dead or unnecessary code in the editor. A diagnostic rule set to None, however, is effectively turned off. Instances of this diagnostic may not be produced at all; even if they are suppressed rather than being made available to the host.


Compilers Analyzer Config (`docs/compilers/analyzer-config.md`)

Compiler Support for AnalyzerConfig

AnalyzerConfig is an EditorConfig-superset (https://editorconfig.org/) file
format recognized by the Roslyn command line compiler. The options specified
in analyzer config files are recognized by the compiler in two ways: option
keys following the pattern dotnet_diagnostic.<diagnostic-id>.severity = <value> are parsed and interpreted by the compiler to configure the severity
of compiler diagnostics. <diagnostic-id> represents the diagnostic ID
matched by the compiler, case-insensitively, to be configured. <value> must
be the name of a member of the
ReportDiagnostic
enum, also case-insensitive. These settings are then applied on a
per-SyntaxTree to each of the files whose path matched the AnalyzerConfig
name specification in the compilation.

Any properties which do not have the aforementioned pattern are considered
analyzer options and are placed in a PerTreeOptionsProvider on the
AnalyzerOptions
type
for
use by analyzers.

AnalyzerConfig files can be passed to the command-line compiler through the
/analyzerconfig:<file-path> parameter.


Compilers Terrapin (`docs/compilers/terrapin.md`)

.NET and Terrapin Process

The Terrapin system is concerned with validating the provenance of NuPkgs in the .NET ecosystem. Specifically it needs to verify that DLLs in a NuPkg can be rebuilt using publicly available artifacts.

This document describes the workflow that will be used by Terrapin for validating the provenance of .NET binaries, responsibilities of each team, as well as the tools the .NET team will deliver to enable the service to be successful.

.NET Tools

The .NET Team will deliver a set of tools to the Terrapin team to provide the necessary validation. The idea in providing a series of tools here vs. a single all in one tool is to help better separate out the different responsibilities involved here and letting the appropriate team handle them:

  1. Providing the list of artifacts necessary for validation to succeed (owned by .NET)
  2. Acquiring the artifacts necessary for validation (owned by Terrapin)
  3. Performing validation and making a determination of provenance (owned by .NET)

It's possible in the future we will end up providing a "all in one" experience tool but for the short term we'll be focusing on keeping these responsibilities and tools separate.

dotnet-build-manifest-generator

This is global tool which takes in a DLL / PDB file combination and will generate an artifacts manifest file which lists the set of artifacts necessary to perform a validation. This includes the version of dotnet-build-validator which needs to be used to perform the validation.

This tool is meant to work with binaries produced by earlier versions of the .NET compiler. Every time the contents of the PDB provenance metadata change a new version of this tool will be produced that can account for the metadata. The expectation is that Terrapin will always have the latest version of this tool installed on their service.

dotnet-build-validator

A global tool which takes in the following arguments: the PE file to validate, its accompanying PDB and a directory containing the artifacts gathered from the artifacts manifest file. The tool will then return whether the provided PDB file can have it's provenance validated.

The tool will have three output states:

  1. Validation succeeded
  2. Validation failed because the binaries do not match
  3. Validation failed because a binary could not be produced

This tool will be sim-shipped with the compiler: every time there is a new compiler, there will be a new version of this tool that uses those compiler binaries. This is important because the compiler does not guarantee deterministic output between versions of the compiler. Even minor version differences of the compiler can produce different IL for the same source code if the code intersects a bug fix or optimization. In order for the validator to function with high fidelity Terrapin must use the version that sim-shipped with the compiler.

The expectation is that Terrapin will install this as a local tool for a given validation event. That will allow for every validation event to use a different version of dotnet-build-validator without having to worry about version conflicts as it would if it were installed as a global tool.

The Terrapin Service Workflow

The Terrapin service will validate a DLL by going through the following workflow for every DLL in a NuPkg file:

  1. Execute dotnet-build-manifest-generator on the DLL + PDB combination.
  2. Setup the environment for validation:
    1. Transfer execution to the appropriate operating system listed in the manifest. All future actions are expected to run on this machine.
    2. Install the correct .NET Runtime on the target machine (if necessary)
  3. Download the artifacts listed in the artifact manifest file as well as the specified version of dotnet-build-validator.
  4. Execute dotnet-build-validator providing the DLL, PDB and the directory where the artifacts were downloaded too.

The Terrapin service will download the artifacts into a directory created by Terrapin. The directory will have two directories to store source files and references respectively: sources and references. The name of the artifacts in those directories will be specified in the manifest file. That will put the burden of avoiding collisions and dealing with file path fixup on the compiler team.

Artifacts Manifest File

The goal of dotnet-build-manifest-generator is to provide a manifest file to the Terrapin service which lists all of the artifacts necessary for a validation to be completed as well as the environment needed to run the validation. The set of artifacts that are necessary include:

  1. The source files used in the compilation.
  2. The reference DLLs used in the compilation.

At this time the expectation is Terrapin will need to recreate much of the environment used to create the binaries in order to perform provenance validation. This includes having a matching .NET Runtime version and operating system. This is because OS dependencies, like zip compression version, can impact the deterministic output of the compiler. It's possible our experiments in Terrapin will cause us to re-evaluate how these play into determinism but for now the design is to build validation on top of our existing assumptions here.

To facilitate this the artifact manifest file will include all of the necessary environment information. This will include the OS RID, the .NET Runtime version and whether a desktop or NET Core runtime was used to run the compiler.

The artifacts manifest file will be a JSON file with the following format:

json
{
    "sources": [
        { 
            "uri": "https://github.com/dotnet/roslyn/blob/f5517311e37a3883e09dc6daa5069bbc4f44c882/src/Compilers/Shared/Csc.cs",
            "destFileName": "c6fea380dac7392b945fa6c63466746f830c5cc3"
        }
    ],
    "references": [
        {
            "name": "System.Reflection.Metadata.dll",
            "mvid": "51ef3ce0-5a89-4325-b094-8a5970acc676",
            "destFileName": "51ef3ce0-5a89-4325-b094-8a5970acc676",
        }
    ],
    "environment": {
        "compiler-language": "csharp",
        "compiler-version": "3.8.0",
        "os": "windows",
        "os-version": "...",
        "dotnet-runtime": "net5.0.0"
    }
}

Phases of validation

There are still many open questions surrounding this effort like the impact of code pages, line endings as well as how strongly .NET runtime versions impact determinism of our outputs. The best way to answer these questions is to be pushing real customer code through the service to prove out the real impact of these problems. There are also just general process issues we need to work out between like cadence of delivery as well as just making sure all the pieces fit together.

To help solve both of these we will be breaking this effort up into a series of milestones that increase in complexity. Each one moving us closer to our goals around provenance of all major NuPkgs in the ecosystem:

The milestones are:

  • Roslyn can successfully validate dotnet/roslyn on a machine that just built dotnet/roslyn. This will give us confidence the underlying provenance checks are achievable. All of the artifacts necessary to perform validation will be located on the machine in well known locations hence that will allow us to separate out "acquisition of artifacts"
  • Roslyn will produce versions of dotnet-build-manifest-generator and dotnet-build-validator for Terrapin and they will validate that they can use them on projects they are hosting locally.
  • Rebuild of DLLs on Windows: the target of this milestone is NuPkgs produced on Windows using the .NET Framework compiler. The goal is for us to be able to rebuild the DLLs, whether validation succeeds or fails. This will help us ensure that we have the right process in place between Terrapin and .NET as well that the PDBs do truly contain enough information for us to recreate compilations.
  • Validation of DLLs on Windows: the target of this milestone is NuPkgs produced on Windows using the .NET Framework compiler. The goal is for us to validate the provenance of these binaries as well as find common reasons why provenance validation fails. This will let us begin the conversation with .NET customers on how to change their build to make it provenance friendly.
  • Validation of DLLs on Windows Part 2: the target of this milestone is NuPkgs produced on Windows using the .NET Core compiler. This will require Terrapin to take the extra environment setup step of installing the correct .NET Core runtime and using that to drive the installation process.
  • Validate binaries produced on Linux / Mac: the target of this milestone is NuPkgs produced on Linux / Mac. This will require Terrapin to involve multiple machines in the validation process. It will also help us understand the true impact of OS sub-versions in our ability to do provenance validation.

FAQ

What about binaries produced before VS 16.7 was released?

The provenance validation process depends on extra metadata included in the PDB that the compiler uses to generate the artifacts manifest file as well as recreate the compilation. That extra information was first added into PDBs in the Visual Studio 16.7 release. That means any binaries produced before then will fail provenance validation.

This is a known issue and the understanding is such older NuPkgs will not be supported for provenance validation.

How do we handle source generators?

The provenance validation will occur using the output of source generators rather than re-running source generators as part of the validation. This will simplify the process as well as remove the issue of determinism in source generators (customer defined tools that don't have our level of rigor) from the process.

Open Issues

Running the validator on .NET Framework

The expectation is that for Windows we should be able to use .NET Core to run the validator for binaries that were produced using the .NET Framework compiler. The runtime differences we are worried about for Unix operating systems don't have the same problems on Windows. Hence any .NET Runtime which can execute the compiler should be able to do provenance validation.

It's possible we will find differences here though in the real world. In which case we will need to have a mode for dotnet-build-validator where by it will shell out to a .NET Framework process to do the validation.

Line ending issues

In order for validation to succeed it is important for dotnet-build-validator to receive source files that have the same line endings as the one's provided to the original compilation. These factor into items like the checksum of source files stored in PDBs, the content of string literals, etc ...

It's possible that the information provided by source link is not sufficient to properly recreate files with correct line endings. Consider that a .gitattributes file could change how line endings are used on different operating systems.

This is a problem the debugger team hits today and has solved with a simple algorithm we can replicate here. Essentially if the file has consistent LF or CRLF line endings and the SHA don't match, flip the line endings. If that matches then use that version.

Future expansions

This document is written to describe how validation will work for the C# and VB compilers but those are not the only tools in the .NET SDK which create or modify DLLs. Other tools include, but is not limited to, the F# compiler and IL trimmer.

The expectation is these tools can be on boarded with minimal changes to the process outlined here. For example the artifact manifest file can be expanded to include new artifact types, new tools can be consumed by Terrapin to do the validation of various phases of the build.

What names should we call the tools

As a rule, @jaredpar is terrible at picking names. Should we pick better names for the tools?


Contributing API Review Process (`docs/contributing/API Review Process.md`)

API Review Process

.NET has a long-standing history of taking API usability extremely seriously. Thus, we generally review every single API that is added to the product. This page discusses how we conduct design reviews for the Roslyn components.

Which APIs should be reviewed?

The rule of thumb is that we (dotnet/roslyn) review every public API that is being added to this repository in the CodeAnalysis (compiler), Workspaces, Features, and EditorFeatures libraries. APIs in the LanguageServices dlls are not usually reviewed as they are not intended for general consumption; they are for use in Visual Studio, and we don't have a compat bar for these APIs.

Steps

  1. Requester files an issue. The issue description should contain a speclet that represents a sketch of the new APIs, including samples on how the APIs are being used. The goal isn't to get a complete API list, but a good handle on how the new APIs would roughly look like and in what scenarios they are being used. Please use this template. The issue should have the labels Feature Request and Concept-API. Here is a good example of a strong API proposal.

  2. We assign an owner. We'll assign a dedicated owner from our side that sponsors the issue. This is usually the area owner for which the API proposal or design change request was filed.

  3. Discussion. The goal of the discussion is to help the assignee to decide whether we want to pursue the proposal or not. In this phase, the goal isn't necessarily to perform an in-depth review; rather, we want to make sure that the proposal is actionable, i.e. has a concrete design, a sketch of the APIs and some code samples that show how it should be used. If changes are necessary, the owner will set the label api-needs-work. To make the changes, the requester should edit the top-most issue description. This allows folks joining later to understand the most recent proposal. To avoid confusion, the requester can maintain a tiny change log, like a bolded "Updates:" followed by a bullet point list of the updates that were being made. When you the feedback is addressed, the requester should notify the owner to re-review the changes.

  4. Owner makes decision. When the owner believes enough information is available to make a decision, they will update the issue accordingly:

    • Mark for review. If the owner believes the proposal is actionable, they will label the issue with api-ready-for-review.
    • Close as not actionable. In case the issue didn't get enough traction to be distilled into a concrete proposal, the owner will close the issue.
    • Close as won't fix as proposed. Sometimes, the issue that is raised is a good one but the owner thinks the concrete proposal is not the right way to tackle the problem. In most cases, the owner will try to steer the discussion in a direction that results in a design that we believe is appropriate. However, for some proposals the problem is at the heart of the design which can't easily be changed without starting a new proposal. In those cases, the owner will close the issue and explain the issue the design has.
    • Close as won't fix. Similarly, if proposal is taking the product in a direction we simply don't want to go, the issue might also get closed. In that case, the problem isn't the proposed design but in the issue itself.
  5. API gets reviewed. The group conducting the review is called RAR, which stands for Roslyn API Reviewers. In the review, we'll take notes and provide feedback. After the review, we'll publish the notes in the API Review repository and at the end of the relevant issue. Multiple outcomes are possible:

    • Approved. In this case the label api-ready-for-review is replaced with api-approved.
    • Needs work. If we believe the proposal isn't ready yet, we'll replace the label api-ready-for-review with api-needs-work.
    • Rejected. If we believe the proposal isn't a direction we want to pursue, we'll simply write a comment and close the issue.

Review schedule

There are three methods to get an API review (this section applies to API area owners, but is included for transparency into the process):

  • Get into the backlog. Generally speaking, filing an issue in dotnet/roslyn and applying the label api-ready-for-review on it will make your issue show up during API reviews. The downside is that we generally walk the backlog oldest-newest, so your issue might not be looked at for a while. Progress of issues can be tracked via https://apireview.net/backlog?g=Roslyn.
  • Fast track. If you need to bypass the backlog apply both api-ready-for-review and blocking. All blocking issues are looked at before we walk the backlog.
  • Dedicated review. If an issue you are the area owner for needs an hour or longer, send an email to roslynapiowners and we book dedicated time. We also book dedicated time for language feature APIs: any APIs added as part of a new language feature need to have a dedicated review session. Rule of thumb: if the API proposal has more than a dozen APIs, the APIs have complex policy, or the APIs are part of a feature branch, then you need 60 min or more. When in doubt, send mail to roslynapiowners.

The API review board currently meets every 2 weeks.

Pull requests

Pull requests against dotnet/roslyn that add new public API shouldn't be submitted before getting approval. Also, we don't want to get work in progress (WIP) PR's. The reason being that we want to reduce the number pending PRs so that we can focus on the work the community expects we take action on.

If you want to collaborate with other people on the design, feel free to perform the work in a branch in your own fork. If you want to track your TODOs in the description of a PR, you can always submit a PR against your own fork. Also, feel free to advertise your PR by linking it from the issue you filed against dotnet/roslyn in the first step above.

For dotnet/roslyn team members, PRs are allowed to be submitted and merged before a full API review session, but you should have sign-off from the area owner, and the API is expected to be covered in a formal review session before being shipped. For such issues, please work with the API area owner to make sure that the issue is marked blocking appropriately so it will be covered in short order.


Contributing Building, Debugging, And Testing On Unix (`docs/contributing/Building, Debugging, and Testing on Unix.md`)

Building, Debugging and Testing on Unix

This guide is meant to help developers setup an environment for debugging / contributing to Roslyn from Linux.
Particularly for developers who aren't experienced with .NET Core development on Linux.

Working with the code

  1. Ensure the commands git and curl are available
  2. Clone [email protected]:dotnet/roslyn.git
  3. Run ./build.sh --restore
  4. Run ./build.sh --build

Working in Visual Studio Code

  1. Install VS Code
    • After you install VS Code, install the C# extension
    • Important tip: You can look up editor commands by name by hitting Ctrl+Shift+P, or by hitting Ctrl+P and typing a > character. This will help you get familiar with editor commands mentioned below. On a Mac, use ⌘ instead of Ctrl.
  2. Install the .NET SDK which matches the sdk.version property in global.json
  3. You can build from VS Code by running the Run Build Task command, then selecting an appropriate task such as build or build current project (the latter builds the containing project for the current file you're viewing in the editor).
  4. You can run tests from VS Code by opening a test class in the editor, then using the Run Tests in Context and Debug Tests in Context editor commands. You may want to bind these commands to keyboard shortcuts that match their Visual Studio equivalents (Ctrl+R, T for Run Tests in Context and Ctrl+R, Ctrl+T for Debug Tests in Context).
  5. You can launch a new VS Code instance with the language server from your current code by running the "launch vscode with language server" task.

Running Tests

After building, the unit tests can be executed by running ./test.sh, which runs the already-built test assemblies via src/Tools/RunTests.

For a combined build and test run, use ./build.sh --test, ./build.sh --testSet:compiler, ./build.sh --testKind:ioperation, or ./build.sh --testFramework:core (single-hyphen spellings are also accepted). Each test option independently requests testing. These shortcuts forward the build configuration and any supplied set, kind, and framework values to RunTests; RunTests selects and validates the tests. For example, ./build.sh --configuration Release --testSet:compiler --testFramework:core --testKind:usedassemblies builds and tests Release compiler binaries with used-assembly validation. Direct calls to ./eng/build.sh use already-built binaries unless --build is also specified.

Repeat --testFramework:<name> to forward multiple framework selections, for example --testFramework:core --testFramework:desktop; do not combine them into a comma-separated value. RunTests determines which frameworks are supported on the current platform.

Pass other test options to test.sh. For example, ./test.sh --testFramework:core --testConfiguration Release --testSet:compiler tests the Release compiler binaries. Use --testKind:ioperation, --testKind:runtimeasync --testFramework:core, or --testKind:usedassemblies for additional validation, and --env:KEY=VALUE to set an environment variable in test processes. Runtime-async validation requires Core-only selection; the default selection includes desktop and is rejected. Run ./test.sh --help for all options.

To run all tests in a single project, it's recommended to use the dotnet test path/to/project command.

GitHub

The best way to clone and push is to use SSH. On Windows you typically use HTTPS and this is not directly compatible
with two factor authentication (requires a PAT). The SSH setup is much simpler and GitHub has a great HOWTO for
getting this setup.

https://help.github.com/articles/connecting-to-github-with-ssh/

Debugging test failures

The best way to debug is using lldb with the SOS plugin. This is the same SOS as used in WinDbg and if you're familiar
with it then lldb debugging will be pretty straight forward.

The dotnet/diagnostics repo has more information:

CoreCLR also has some guidelines for specific Linux debugging scenarios:

Corrections:

  • LLDB and createdump must be run as root
  • dotnet tool install -g dotnet-symbol must be run from $HOME

Core Dumps

The CoreClr does not used the standard core dumping mechanisms on Linux. Instead you must specify via
environment variables that you want a core dump to be produced. The simplest setup is to do the following:

text
> export COMPlus_DbgEnableMiniDump=1
> export COMPlus_DbgMiniDumpType=4

This will cause full memory dumps to be produced which can then be loaded into LLDB.

A preview of dotnet-dump is also available for interactively creating and analyzing dumps.

GC stress failures

When you suspect there is a GC failure related to your test then you can use the following environment variables
to help track it down.

text
> export COMPlus_HeapVerify=1
> export COMPlus_gcConcurrent=1

The COMPlus_HeapVerify variable causes GC to run a verification routine on every entry and exit. Will crash with
a more actionable trace for the GC team.

The COMPlus_gcConcurrent variable removes concurrency in the GC. This helps isolate whether this is a GC failure
or memory corruption outside the GC. This should be set after you use COMPLUS_HeapVerify to determine it is
indeed crashing in the GC.

Note: this variables can also be used on Windows as well.

Ubuntu 18.04

The recommended OS for developing Roslyn is Ubuntu 18.04. This guide was written using Ubuntu 18.04 but should be
applicable to most Linux environments. Ubuntu 18.04 was chosen here due to it's support for enhanced VMs in Hyper-V.
This makes it easier to use from a Windows machine: full screen, copy / paste, etc ...

Hyper-V

Hyper-V has a built-in Ubuntu 18.04 image which supports enhanced mode. For more information, see a tutorial for creating such an image.

When following this make sure to:

  1. Click Installation Source and uncheck "Windows Secure Boot"
  2. Complete the Ubuntu installation wizard. Full screen mode won't be available until this is done.

Overall this takes about 5-10 minutes to complete.

Many of the repositories that need to be built use source link and it crashes on Ubuntu 18.04 due to dependency changes.
To disable source link add the following to the Directory.Build.props file in the root of the repository.

xml
<EnableSourceControlManagerQueries>false</EnableSourceControlManagerQueries>
<EnableSourceLink>false</EnableSourceLink>
<DeterministicSourcePaths>false</DeterministicSourcePaths>

Prerequisites

Make sure to install the following via apt install

  • clang
  • lldb
  • cmake
  • xrdp

Troubleshooting

See the Troubleshooting section
in the Windows doc, which is not Windows-specific (use rm -rf in place of Remove-Item).


Contributing Building, Debugging, And Testing On Windows (`docs/contributing/Building, Debugging, and Testing on Windows.md`)

Building, Debugging and Testing on Windows

Working with the code

Using the command line, Roslyn can be developed using the following pattern:

  1. Clone https://github.com/dotnet/roslyn
  2. Run Restore.cmd
  3. Run Build.cmd
  4. Run Test.cmd

The minimal required version of .NET Framework is 4.7.2.

Developing with Visual Studio

  1. Use latest Visual Studio Insiders
    • Ensure Visual Studio extension development is included in the selected workloads
    • Ensure C# and Visual Basic, MSBuild, and .NET Core are included in the selected individual components
    • Restart Visual Studio
  2. Install the .NET SDK which matches the sdk.version property in global.json
  3. PowerShell 5.0 or newer. If you are on Windows 10 or newer, you are fine; you'll only need to upgrade if you're on earlier versions of Windows. The download link is under the "Upgrading existing Windows PowerShell" heading.
  4. Run Restore.cmd
  5. Open Roslyn.slnx

Developing with Visual Studio Code

See the Building, Debugging, and Testing on Unix documentation to get started developing Roslyn using Visual Studio Code.

Running Tests

There are a number of options for running the core Roslyn unit tests:

Command Line

The Test.cmd script runs unit tests on already-built test binaries via src/Tools/RunTests. Run Build.cmd separately before testing, and pass test options to Test.cmd.

For a combined build and test run, use Build.cmd -test, Build.cmd -testSet:compiler, Build.cmd -testKind:ioperation, or Build.cmd -testFramework:core. Each test option independently requests testing. These shortcuts forward the build configuration and any supplied set, kind, and framework values to RunTests; RunTests selects and validates the tests. For example, Build.cmd -configuration Release -testSet:compiler -testFramework:core -testKind:usedassemblies builds and tests Release compiler binaries with used-assembly validation. Direct calls to eng\build.ps1 use already-built binaries unless -build is also specified. Other test options belong on Test.cmd.

To select multiple frameworks from PowerShell, pass an array: .\eng\build.ps1 -build -testFramework:core,desktop. Each array element is forwarded as a separate RunTests framework option; do not repeat the named PowerShell parameter.

  1. Run the "Developer PowerShell for VS Insiders" or "Developer Command Prompt for VS Insiders" from your start menu, alternatively launch it from Visual Studio, it can be found at "Tools -> Command Line -> Developer Powershell/Developer Command Prompt".
  2. Navigate to the directory of your Git clone.
  3. Run Test.cmd in the command prompt.

Pass options directly to Test.cmd: --testFramework:desktop selects .NET Framework, while --testFramework:core selects .NET. Repeat --testFramework to run both. Use --testConfiguration Release to test Release binaries and --testPlatform x86 to select x86. Run Test.cmd --help for all options.

Visual Studio integration tests have a separate entry point: run eng\test-vsi.ps1 -configuration Debug after building.

The results of the tests can be viewed in the artifacts/TestResults directory.

Test Explorer

Tests can be run and debugged from the Test Explorer window. For best performance, we recommend the following:

  1. Open Tools → Options... → Test
    1. Check the box for Discover tests in real time from source files
    2. Uncheck the box for Additionally discover tests from build assemblies...
  2. Use the Search box of Test Explorer to narrow the scope of visible tests to the feature(s) you are working on
  3. When you are not actively running tests, set the search query to __NonExistent__ to hide all tests from the UI

Testing in Linux using WSL

Tests can be run and debugged under WSL. This requires a bit of setup the first time. After that, it is as easy as selecting WSL as the active environment in Test Explorer.

  1. Install WSL by running wsl --install (details)

  2. In the VS installer, install ".NET Debugging with WSL" as an individual component

  3. In Test Explorer, go into "Configure Remote Test Environments" (under the gear icon) and uncomment the wsl/Ubuntu environment:

  4. Select that test environment from Test Explorer drop-down:

  5. Run a test from Test Explorer test list. This will prompt you to install some dotnet bits into the WSL environment the first time.

  6. Debug a test. This will prompt you to install some remote debugging bits into the WSL environment the first time.

More details

WPF Test Runner

To debug through tests, you can right click the test project that contains your
tests and choose Set as Startup Project. Then press F5. This will run the
tests under the command line runner. Some members of the team have been
working on a GUI runner that allows selection of individual tests, etc. Grab
the source from
xunit.runner.wpf, build it and
give it a try.

Trying Your Changes in Visual Studio

You can build and deploy with the following command:
.\Build.cmd -Restore -Configuration Release -deployExtensions -launch.

Then you can launch the RoslynDev hive with devenv /rootSuffix RoslynDev.

Deploying with F5

The Rosyln solution is designed to support easy debugging via F5. Several of our
projects produce VSIX which deploy into Visual Studio during build. The F5 operation
will start a new Visual Studio instance using those VSIX which override our installed
binaries. This means trying out a change to the language, IDE or debugger is as
simple as hitting F5. Note that for changes to the compiler, out-of-process builds won't use the privately built version of the compiler.

The startup project needs to be set to RoslynDeployment. This should be
the default but in some cases will need to be set explicitly. To set it, right-click
the RoslynDeployment project in Solution Explorer and select "Set as Startup Project".

RoslynDeployment is a container project located in the src/Deployment folder that
bundles and deploys all the main Roslyn extensions together. When you press F5 with
RoslynDeployment set as the startup project, it will deploy all of the following extensions
at once, giving you a complete debugging experience with all Roslyn components.

If you're working on a specific area and want to optimize build times by deploying only
the relevant extension, you can set one of the individual projects below as your startup
project instead:

  • Roslyn.VisualStudio.Setup: this project can be found inside the VisualStudio\Setup folder
    from the Solution Explorer, and builds Roslyn.VisualStudio.Setup.vsix. It
    contains the core language services that provide C# and VB editing. It also
    contains the copy of the compiler that is used to drive IntelliSense and
    semantic analysis in Visual Studio. Although this is the copy of the compiler
    that's used to generate squiggles and other information, it's not the
    compiler used to actually produce your final .exe or .dll when you do a
    build. If you're working on fixing an IDE bug, this is the project you want
    to use.
  • Roslyn.Compilers.Extension: this project can be found inside the Compilers\Extension folder
    from the Solution Explorer, and builds Roslyn.Compilers.Extension.vsix.
    This deploys a copy of the command line compilers that are used to do actual
    builds in the IDE. It only affects builds triggered from the Visual Studio
    experimental instance it's installed into, so it won't affect your regular
    builds. Note that if you install just this, the IDE won't know about any
    language features included in your build. If you're regularly working on new
    language features, you may wish to consider building both the
    CompilerExtension and VisualStudioSetup projects to ensure the real build and
    live analysis are synchronized.
  • ExpressionEvaluatorPackage: this project can be found inside the
    ExpressionEvaluator\Package folder from the Solution Explorer, and builds
    ExpressionEvaluatorPackage.vsix. This deploys the expression evaluator and
    result providers, the components that are used by the debugger to parse and
    evaluate C# and VB expressions in the Watch window, Immediate window, and
    more. These components are only used when debugging.

The experimental instance used by Roslyn is an entirely separate instance of
Visual Studio with it's own settings and installed extensions. It's also, by
default, a separate instance than the standard "Experimental Instance" used by
other Visual Studio SDK projects. If you're familiar with the idea of Visual
Studio hives, we deploy into the RoslynDev root suffix.

Deploying with VSIX and Nuget package

If you want to try your extension in your day-to-day use of Visual Studio, you
can find the extensions you built in your Binaries folder with the .vsix
extension. You can double-click the extension to install it into your main
Visual Studio hive. This will replace the base installed version. Once it's
installed, you'll see it marked as "Experimental" in Tools > Extensions and
Updates to indicate you're running your experimental version. You can uninstall
your version and go back to the originally installed version by choosing your
version and clicking Uninstall.

If you only install the VSIX, then the IDE will behave correctly (ie. new compiler
and IDE behavior), but the Build operation or building from the command-line won't.
To fix that, add a reference to the Microsoft.Net.Compilers.Toolset you built into
your csproj. As shown below, you'll want to (1) add a nuget source pointing to your local build folder,
(2) add the package reference, then (3) verify the Build Output of your project with a
#error version included in your program.

Trying Your Changes in Visual Studio Code

See VSCode docs.

Various other tips

Referencing bootstrap compiler

If you made changes to a Roslyn compiler and want to build any projects with it, you can either
use the Visual Studio hive where your CompilerExtension is installed, or from
command line, run msbuild with /p:BootstrapBuildPath=YourBootstrapBuildPath.
YourBootstrapBuildPath could be any directory on your machine so long as it had
csc and vbc inside it. You can check the cibuild.cmd and see how it is used.

Troubleshooting your setup

To confirm what version of the compiler is being used, include #error version in your program
and the compiler will produce a diagnostic including its own version as well as the language
version it is operating under.

You can also attach a debugger to Visual Studio and check the loaded modules, looking at the folder
where the various CodeAnalysis modules were loaded from (the RoslynDev should load them somewhere
under AppData, not from Program File).

Testing on the [dotnet/runtime](https://github.com/dotnet/runtime) repo

  1. Make sure that you can build the runtime repo as baseline (run build.cmd libs+libs.tests, which should be sufficient to build all C# code, installing any prerequisites if prompted to, and perhaps git clean -xdf and build.cmd -restore initially - see runtime repo documentation for specific prerequisites and build instructions)
  2. build.cmd -pack -c Release on your roslyn repo
    • Note that -c Debug can also be used (along with changing Release to Debug in RestoreAdditionalProjectSources property value below). This will allow checking the compiler's debug assertions when building the runtime.
    • It's good for us to investigate scenarios where compiling the runtime libraries causes the compiler's debug assertions to fail. However, assertion failures can obscure whether or not the compiler ultimately succeeds at building the runtime and producing correct binaries. So, if the goal is to only check for "functional breaks", then using a Release mode compiler to start with can be preferable.
  3. Find the compiler toolset in your NuGet package cache (its location is likely %NUGET_PACKAGES%\microsoft.net.compilers.toolset or %userprofile%\.nuget\packages\microsoft.net.compilers.toolset).
  4. If there is a toolset version in the NuGet cache with the same version as the toolset you just packed in Roslyn, delete the toolset version from the cache. This allows NuGet to pick up the new packages you just created locally instead of using a stale cached version.
  5. Modify your local enlistment of runtime similarly to this commit then build again
    • add <RestoreAdditionalProjectSources>\artifacts\packages\Release\Shipping\</RestoreAdditionalProjectSources> using the local path to your roslyn repo to Directory.Build.props
    • add true and <MicrosoftNetCompilersToolsetVersion>5.3.0-dev</MicrosoftNetCompilersToolsetVersion> with the package version you just packed (look in above artifacts folder) to eng/Versions.props

Testing a VS insertion

See internal documentation for that process here.

Testing with extra IOperation validation

Run Test.cmd --testFramework:core --testSet:compiler --testKind:ioperation, which sets the ROSLYN_TEST_IOPERATION environment variable to true in the test processes.
For running those tests in an IDE, the easiest is to find the //#define ROSLYN_TEST_IOPERATION directive and uncomment it.
See more details in the IOperation test hook doc.

Replicating Failures in the Used Assemblies leg

In order to replicate test failures in that leg, there are a few options:

  1. Uncomment src/Compilers/Test/Core/Compilation/CompilationExtensions.cs:9, which defines ROSLYN_TEST_USEDASSEMBLIES, and run your tests. Do not check this in, as it
    will enable the test hook for every test in every project and significantly slow down regular test runs.
  2. Set the ROSLYN_TEST_USEDASSEMBLIES environment variable and restart VS with it set.
  3. Set a breakpoint at the start of CSharpTestBase.VerifyUsedAssemblyReferences in src/Compilers/Test/Utilities/CSharp/CSharpTestBase.cs. When it breaks, use VS's jump to location or
    drag the instruction pointer past the early check and return on EnableVerifyUsedAssemblies.

When a test failure is isolated, please add a dedicated test for this (ie. failing even when the Used Assemblies validation isn't enabled) to make it easier to avoid future regressions.
Preferrably, don't replicate the entire original test, just enough to hit the bug to ensure that it's protected against regressions.
Before pushing a relevant fix to CI, validate locally using Test.cmd --testFramework:core --testSet:compiler --testKind:usedassemblies.

Running the PublicAPI fixer

  1. Restore and build Compilers.slnf. This is necessary to ensure the source generator project is built and we can load the generator assembly when running dotnet format.
    ps1
    C:\Source\roslyn> .\restore.cmd
    C:\Source\roslyn> .\build.cmd
  2. Invoke dotnet format (the one included in .NET SDK, not the global tool dotnet-format which is deprecated). Running only the analyzers subcommand and fixing only the "missing Public API signature" diagnostic. We must also pass the --include-generated flag to include source generated documents in the analysis.
    ps1
    C:\Source\roslyn> cd ..
    C:\Source> dotnet format analyzers .\roslyn\Compilers.slnf --diagnostics=RS0016 --no-restore --include-generated -v diag

Troubleshooting

If you hit an unexpected build failure, especially one that doesn't reproduce on a clean
clone, a good first step is to delete the artifacts folder and rebuild. If that doesn't
help, the bigger hammer is git clean -xdfn (dry run) followed by git clean -xdf. Note
that this also deletes the locally-provisioned .dotnet SDK, forcing a full restore and
rebuild, so prefer just deleting artifacts first.

For example, renaming or moving your clone can leave stale cached files under
artifacts\obj\...\*.xlf\ referencing the old path, which causes Build.cmd to fail with
errors like:

text
error MSB3103: Invalid Resx file. Could not find a part of the path
'<OLD-PATH>\src\RoslynAnalyzers\Text.Analyzers\Core\Dictionary.dic'. [Text.Analyzers.csproj]

Deleting artifacts and rebuilding should fix the issue.
See
dotnet/arcade issue 17061
for details.

Contributing

Please see Contributing Code for details on contributing changes back to the code.


Contributing Compiler Test Plan (`docs/contributing/Compiler Test Plan.md`)

Compiler Test Plan

This document provides guidance for thinking about language interactions and testing compiler changes.

General concerns

  • Completeness of the specification as a guide for testing (is the spec complete enough to suggest what the compiler should do in each scenario?)
  • Ping for new breaking changes and general ping for partner teams (Bill, Kathleen, Mads, IDE, Razor)
  • Help review external documentation
  • Backward and forward compatibility (interoperation with previous and future compilers, each in both directions)
  • Error handling/recovery (missing libraries, including missing types in mscorlib; errors in parsing, ambiguous lookup, inaccessible lookup, wrong kind of thing found, instance vs static thing found, wrong type for the context, value vs variable)
  • 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)
  • 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
    • GetSymbolInfo
    • GetSpeculativeSymbolInfo
    • GetTypeInfo
    • GetSpeculativeTypeInfo
    • GetMethodGroup
    • GetConstantValue
    • GetAliasInfo
    • GetSpeculativeAliasInfo
    • LookupSymbols
    • AnalyzeStatementsControlFlow
    • AnalyzeStatementControlFlow
    • AnalyzeExpressionDataFlow
    • AnalyzeStatementsDataFlow
    • AnalyzeStatementDataFlow
    • ClassifyConversion
    • GetOperation (IOperation)
    • GetCFG (ControlFlowGraph), including a scenario with some nested conditional
    • DocumentationCommentId APIs
  • 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

See also types and members lists below.

  • Access modifiers (public, protected, internal, protected internal, private protected, private), static, ref
  • type declarations
    • class with or without primary constructor
    • record class/struct with or without positional members
    • struct with or without primary constructor
    • union
    • interface
    • type parameter
  • file-local types
  • methods
  • 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)
  • Attributes (including generic attributes and security attributes)
    • Compiler-recognized attributes should not have any effect in earlier LangVersions,
      except a LangVersion error should be reported when functionality depending on the attribute is used
      (for example, InlineArray conversion to Span).
  • Generics (type arguments, variance, constraints including class, struct, new(), unmanaged, notnull, allows ref struct, types and interfaces with nullability)
  • Default and constant values
  • Partial classes
  • Literals
  • Enum (implicit vs. explicit underlying type)
  • Expression trees
  • Iterators
  • Initializers (object, collection, dictionary)
  • Array (single- or multi-dimensional, jagged, initializer, fixed)
  • Expression-bodied methods/properties/...
  • Extension methods
  • Partial method
  • Named and optional parameters
  • String interpolation
  • Raw strings (including interpolation)
  • Properties (read-write, read-only, init-only, write-only, auto-property, expression-bodied)
  • Interfaces (implicit vs. explicit interface member implementation)
  • Delegates
  • Multi-declaration
  • NoPIA
  • Dynamic
  • Ref structs, Readonly structs
  • Readonly members on structs (methods, property/indexer accessors, custom event accessors)
  • SkipLocalsInit
  • Method override or explicit implementation with where T : { class, struct, default }
  • extension blocks (emitted with content-based names):
    • methods
    • operators (except conversion operators)
    • properties and indexers (including get/set/init accessors)

Code

  • Operators (see Eric's list below)
  • Lambdas (capture of parameters or locals, target typing)
  • Execution order
  • Target typing (var, lambdas, integrals)
  • Conversions (boxing/unboxing)
  • Nullable (wrapping, unwrapping)
  • Overload resolution, override/hide/implement (OHI)
    • OverloadResolutionPriorityAttribute
  • Inheritance (virtual, override, abstract, new)
  • Anonymous types
  • Tuple types and literals (elements with explicit or inferred names, long tuples), tuple equality
  • Range literals (1..2) and Index operator (^1)
  • Deconstructions
  • Local functions
  • Unsafe code
  • LINQ
  • Async (task-like types) and async-iterator methods
  • Lvalues: the synthesized fields are mutable
    • Ref / out parameters
    • Compound operators (+=, /=, etc ..)
    • Assignment exprs
  • Ref return, ref readonly return, ref ternary, ref readonly local, ref local re-assignment, ref foreach
  • Ref fields
  • scoped parameters and locals
  • this = e; in struct .ctor
  • Stackalloc (including initializers)
  • Patterns (constant, declaration, var, positional, property and extended property, discard, parenthesized, type, relational, and/or/not, list, slice, constant string matching Span<char>)
  • Switch expressions
  • With expressions (on record classes and on value types)
  • Nullability annotations (?, attributes) and analysis
  • Definite assignment analysis and auto-default struct fields
  • If you add a place an expression can appear in code, make sure SpillSequenceSpiller handles it. Test with a switch expression or stackalloc in that place.
  • If you add a new expression form that requires spilling, test it in the catch filter.
  • If you add a new expression that can be nested inside itself, test deeply nesting it. Potentially cut off direct nesting.
  • extension based Dispose, DisposeAsync, GetEnumerator, GetAsyncEnumerator, Deconstruct, GetAwaiter etc.
  • UTF8 String Literals (string literals with 'u8' or 'U8' type suffix).
  • Inline array element access and slicing.
  • Collection expressions, spread-elements (.. expr), and with-elements ([with(...)]).

Misc

  • reserved keywords (sometimes contextual)
  • pre-processing directives
  • COM interop
  • modopt and modreq
  • CompilerFeatureRequiredAttribute
  • CompilerLoweringPreserveAttribute
  • ref assemblies
  • extern alias
  • UnmanagedCallersOnly
  • telemetry

Testing in interaction with other components

Interaction with IDE, Debugger, and EnC should be worked out with relevant teams. A few highlights:

  • IDE

    • Colorization and formatting
    • Typing experience and dealing with incomplete code
    • Intellisense (squiggles, dot completion)
    • "go to", Find All References, and renaming
    • F1 help (for new keywords)
    • cref comments
    • UpgradeProject code fixer
    • More: IDE Test Plan
  • Debugger / EE

    • Stepping, setting breakpoints
    • Displaying Locals/Autos windows
      • Type and Value display
      • Expanding instance members
      • Locals/parameters/this in closure classes
      • Attributes on locals and expressions (e.g.: [Dynamic], [TupleElementNames])
    • Compiling expressions in Immediate/Watch windows or hovering over an expression
    • Compiling expressions in [DebuggerDisplay("...")]
    • Assigning values in Locals/Autos/Watch windows
  • Edit-and-continue

  • Live Unit Testing (instrumentation)

  • Engage with VS Templates team (if applicable)

Statements

cs
{ … }
;
label : …
T x = whatever; // including `using` and `await` using variants
M();
++x;
x++;
--x;
x--;
a?.M(); // conditional access / null-propagating operator
a?[b] = c;
a?.b = c;
a?[b] = c;
a?.b += c; // and other compound assignment cases
var x = a?.M(); // similar "value is used" versions of the above '?.' cases
new C();
if (…) … else …
switch(…) { … case (…) when (…): … }
while(…) …
do … while(…);
for( … ; … ; … ) …
foreach(…) … // including `await` variant
fixed(…) … // (plain, or custom with `GetPinnableReference`)
goto … ;
throw … ;
return … ;
try { … } catch (…) when (…) { … } finally { … }
checked { … }
unchecked { … }
lock(…) … // including variant on an instance of the `System.Threading.Lock` type
using (…) … // including `await` variant
yield return …;
yield break;
break;
continue;
break label;
continue label;

Expression classifications

Every expression can be classified as exactly one of these:

  • Value
  • Variable
  • Namespace
  • Type
  • Method group
  • Null literal
  • Default literal
  • Anonymous function
  • Property
  • Indexer
  • Event
  • Void-returning method call
  • Array initializer (*)
  • __arglist (*)

(*) Technically not an expression according to the spec.

Note that only values, variables, properties, indexers and events have a type.

Variable classifications

A variable is a storage location. These are all the different ways to refer to a storage location:

  • Static field
  • Instance field
  • Array element
  • Formal param, value
  • Formal param, ref
  • Formal param, out
  • Local variable
  • Pointer dereference
  • __refvalue
  • Inline array access

Operators

c#
x.y
f( )
a[e]
x++ (including instance user defined)
x-- (including instance user defined)
new X()
new()
typeof(T)
default(T)
default
checked(e)
unchecked(e)
delegate ( ) { }
+x
-x
!x
~x
^x
++x (including instance user defined)
--x (including instance user defined)
(X)x
x * y
x / y
x % y
x + y
x - y
x << y
x >> y
x >>> y
x < y
x > y
x <= y
x >= y
x is X
x as X
x == y
x != y
x & y
x ^ y
x | y
x && y
x || y
x ?? y
x ? : y : z
x = y
x *= y (including instance user defined)
x /= y (including instance user defined)
x %= y (including instance user defined)
x += y (including instance user defined)
x -= y (including instance user defined)
x <<= y (including instance user defined)
x >>= y (including instance user defined)
x >>>= y (including instance user defined)
x &= y (including instance user defined)
x ^= y (including instance user defined)
x |= y (including instance user defined)
x ??= y
x => { }
sizeof( )
*x
& x
x->y
e is pattern
e switch { ... }
await x
__arglist( )
__refvalue( x, X )
__reftype( x )
__makeref( x )

Explicit conversions

  • Numeric
  • Enum
  • Nullable
  • Reference
  • Unboxing
  • Dynamic
  • Type parameter
  • User defined
  • Pointer to pointer
  • Pointer to integral
  • Integral to pointer
  • Tuple literal
  • Tuple

Implicit conversions

  • Identity
  • Numeric
  • Literal zero to enum (we actually do constant zero, not literal zero)
  • Nullable
  • Null literal
  • Reference
  • Boxing
  • Dynamic
  • Constant
  • Type parameter
  • User defined
  • Union
  • Anonymous function
  • Method group
  • Pointer to void pointer
  • Null literal to pointer
  • Interpolated string
  • Tuple literal
  • Tuple
  • Default literal
  • Implicit object creation (target-typed new)
  • Function type (in type inference comparing function types of lambdas or method groups)
  • UTF8 String Literal (string constant value to byte[], Span<byte>, or ReadOnlySpan<byte> types)
  • Inline arrays (conversions to Span and ReadOnlySpan)
  • Collection expression conversions
  • Span conversions (from array to (ReadOnly)Span, or from string or (ReadOnly)Span to ReadOnlySpan)

Types

  • Class
  • Interface
  • Delegate
  • Struct
  • Union
  • Enum
  • Nullable
  • Pointer
  • Type parameter

Members

  • Class
  • Struct
  • Union
  • Interface
  • Enum
  • Delegate
  • Namespace
  • Property
  • Event
  • Constructor
  • Destructor
  • Method
  • Interface method
  • Field
  • User-defined indexer
  • User-defined operator (including checked, including instance increment/decrement and compound assignment)
  • User-defined conversion (including checked)

Patterns

  • Discard Pattern
  • Var Pattern
  • Declaration Pattern
  • Constant Pattern
  • Constant string matching Span<char>
  • Recursive Pattern
  • Parenthesized Pattern
  • and Pattern
  • or Pattern
  • not Pattern
  • Relational Pattern
  • Type Pattern
  • Union matching rules

Metadata table numbers / token prefixes

If you look at a 32 bit integer token as a hex number, the first two digits identify the “table number” and the last six digits are an offset into that table. The table numbers are:

text
00 Module
01 TypeRef
02 TypeDef
03 FieldPtr
04 Field
05 MethodPtr
06 Method
07 ParamPtr
08 Param
09 InterfaceImpl
0A MemberRef
0B Constant
0C CustomAttr
0D FieldMarshal
0E DeclSecurity
0F ClassLayout
10 FieldLayout
11 StandAloneSig
12 EventMap
13 EventPtr
14 Event
15 PropertyMap
16 PropertyPtr
17 Property
18 MethodSemantics
19 MethodImpl
1A ModuleRef
1B TypeSpec
1C ImplMap
1D FieldRVA
1E ENCLog
1F ENCMap
20 Assembly
21 AssemblyProcessor
22 AssemblyOS
23 AssemblyRef
24 AssemblyRefProcessor
25 AssemblyRefOS
26 File
27 ExportedType
28 ManifestResource
29 NestedClass
2A GenericParam
2B MethodSpec
2C GenericConstraint

Contributing Developing A Language Feature (`docs/contributing/Developing a Language Feature.md`)

Developing a Language Feature

Adding a new feature to C# or VB is a very serious undertaking that often takes several iterations to complete for even the (seemingly) simplest of features. This is due to both the inherent complexity of changing languages and the need to consider the effects of new features in all layers of the Roslyn codebase: IDE, debugging, scripting, etc. As such, language work occurs in a separate branch until the feature reaches a point when we are ready to merge it into the main compiler.

This page discusses the process by which language feature implementations are considered, prototyped, and fully accepted into the language. This process is intended to be used by the compiler team and community.

Process

  1. Feature specification filed: The speclet should be filed as a GitHub issue and contain:

    text
    * A description of the feature (including any syntax changes involved)
     * Discussions about impacted areas, such as overload resolution and type inference. Think through the major areas of the language specification while determine the potential impacts
     * Proposed changes to the API surface area.

    A feature speclet is different from a language design discussion. Discussions are very open-ended and often for features that simply won't work in the language. A feature speclet is a declaration of intent to do the feature as proposed. In many cases, this will come out of a language design discussion.

    Linking to an early prototype of the proposed feature is highly encouraged. It gives the Language Team a clear understanding of the design intent of the feature and can often serve as the initial commit in the feature branch.

    Note: The goal is not to have a 100%-complete design but instead a general understanding of how the language feature is intended to work.

  2. Discussion: The goal of the discussion is to help clarify the design and determine if the Language Team wants to sanction the proposal. As the discussion evolves the author is encouraged to update the speclet to incorporate the changing design. This allows folks who join later to understand the most recent proposal by reading the top of the thread.

  3. Language team makes a decision: When the Language Team believes enough information is available they will update the issue accordingly:

    • Ready for prototype: The feature is worth pursuing as a prototype for the language. Work continues in the "Prototype" phase.
    • Not actionable: The discussion didn't lead to a design that was actionable by the team.
    • Declined as proposed: The proposal is overall a good idea but the proposed implementation details are not as efficient as they could be. In most cases, the language team will attempt to steer the discussion to a cleaner design but understand that it may be simpler to close and start over with a new issue.
    • Backlog: The feature is a good idea with an acceptable design but the Language Team simply can't allocate the resources at this time for this work.
    • Won't Fix: The feature isn't taking the language in a direction the Language Team is interested in pursuing.

    A Language Team member will be paired with a community-driven feature when it is deemed 'Ready for prototype'. This team member will act as the primary source of contact for the community and will help drive the feature to completion.

  4. Prototype phase: All features are initially prototyped in a separate branch. The name of the branch is features/<feature name>. These branches are created when a feature speclet is marked as 'Ready for prototype', language team member champions the feature, and a developer is assigned to implement the feature. The developer is responsible for keeping an up-to-date speclet of the feature at docs/features/<feature name>.md throughout the development.

    When starting work on a new feature, use the new-compiler-feature Copilot skill (in .github/skills/new-compiler-feature/) to set up tracking: it creates a test plan issue, a Feature - <name> label, updates the Language Feature Status page, and links related PRs. Run it from a PR that implements (part of) the feature.

    All features should be off by default until fully accepted into the language. A new feature should only be enabled when explicitly included in the /features:<feature-name> compiler option. This helps ensure the language is always in a shippable state no matter what prototypes are in the current branch.

  5. Prototype decision: When the prototype phase ends, the Language Team will make a decision on the feature:

    • Ready to integrate: The feature implementation revealed no blocking issues, has a healthy set of test suites, and is sufficiently complete in the compiler, IDE, debugger, etc. to integrate into the main tree on a selected branch. The test suites need to focus both on the feature in isolation and when combined with other areas of the language.
    • Iterate: Experience with the prototype has produced valuable feedback suggesting modifications to the
      speclet. Work reverts to the Discussion state, which may lead to speclet and prototype
      changes (or even abandoning the feature!). This typically occurs either when the original speclet
      left some language details unspecified, to be resolved based
      on implementation experience, or when experience with the prototype suggests that some language design
      decisions should be reconsidered.
    • Closed: The feature hit unforeseen design issues, implementation roadblocks, etc. that caused it to be removed.
  6. Finishing: Once a feature is integrated into the main tree, it's time to lock down and finish the implementation. This includes any remaining areas not completed in the prototype, and completing all of the corner cases and tests (lots and lots of tests!).

    Features which are in the main tree but not tied to a langversion switch are referred to as experimental. They can be pulled or changed at any time.

  7. Acceptance: When the Language Team feels a feature is complete it will be accepted into the language. The corresponding feature flag will be removed and the feature will be tied to the appropriate /langversion option.

It's important to note there are no guarantees about a feature being a part of the language until it has reached the 'Acceptance' state and shipped in an official build. Features in any other state--no matter how complete--can be pulled at the last minute if the Language Team deems it's necessary to do so.

Pulling a feature after the prototype phase completes is not a decision the Language Team takes lightly; however, in the past, unforeseen circumstances have caused us to do so. The best example of this was the decision to pull parameterless struct constructors very late in the C# 6.0 timeframe.

Community Contributions

The intent of this document is to codify the process used by the Language Team to implement language features so that the community can contribute implementations. Community members can contribute to ongoing prototypes or in some cases be the primary driver of the prototype.

Community members who wish to drive a particular feature can request so by commenting on an accepted feature speclet. Even when driven by a community member, a language prototype will occupy a non-trivial amount of time by the Language Team. Hence we will likely only grant this request to community members who have been actively contributing to the project in smaller areas.

Frequently Asked Questions

  • Can feature branches be rebased?: This is left to the discretion of the developer working on the feature. Once the feature is merged into a main branch (main, feature) then rebasing is no longer allowed.

  • Why was my language feature PR closed?: Language features will not be accepted as direct PRs to a main branch. Even simple features are too involved for senior Language Team members to complete in this fashion. Features need to follow the above process in order to be incorporated into the language.

  • Why are features disabled by default?: Language features spend a considerable amount of time in the same branch as our shipping product which is often main. The main branch of Roslyn is kept in a ship ready state at all times. To facilitate this all features which haven't been fully accepted into the language must be disabled.

  • What is the difference between the feature and main branches?: The main branch represents code which will ship in updates to the current version of the compiler. The feature branches represent features that are still in progress, therefore not merged yet.

  • Why weren't speclets filed for early features?: Features such as tuples and local functions were done before this process was fully developed. Their speclets were considered internally and eventually became the docs/feature/<feature name.md> file.

  • Why the high bar to create a feature branch?: Language features, even when driven by the community, require a substantial long term resource investment by the language team. The team must do regular code reviews, provide guidance, help with architectural issues, contribute, etc ...


Contributing Documentation For IDE CodeStyle Analyzers (`docs/contributing/Documentation for IDE CodeStyle analyzers.md`)

Documentation for IDE CodeStyle analyzers

Overview

  1. Official documentation for all IDE analyzer diagnostic IDs is added to dotnet/docs repo at https://github.com/dotnet/docs/tree/main/docs/fundamentals/code-analysis/style-rules.

  2. Each IDE diagnostic ID has a dedicated documentation page. For example:

    1. Diagnostic with code style option: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0011
    2. Diagnostic without code style option: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0004
    3. Multiple diagnostic IDs sharing the same option(s): https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0003-ide0009. In this case, a redirection for all diagnostic IDs must be added.
  3. We have tabular indices for IDE diagnostic IDs and code style options for easier reference and navigation:

    1. Primary index (rule ID + code style options): https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/
    2. Secondary index (code style options only): https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/language-rules#net-style-rules
    3. Indices for each preference/option group. For example:
      1. Modifier preferences: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/modifier-preferences
      2. Expression-level preferences: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/expression-level-preferences

Action items when adding or updating IDE analyzers

Roslyn IDE team (@dotnet/roslyn-ide) is responsible for ensuring that the documentation for IDE analyzers is up-to-date. Whenever we add a new IDE analyzer, add new code style option(s) OR update semantics of existing IDE analyzers the following needs to be done:

  1. Action required in dotnet/docs repo:

    1. If you creating a Roslyn PR for an IDE analyzer:
      1. Please follow the steps at Contribute docs for 'IDExxxx' rules to add documentation for IDE analyzers to dotnet/docs repo.
      2. Ideally, the documentation PR should be created in parallel to the Roslyn PR or immediately following the approval/merge of Roslyn PR.
    2. If you are reviewing a Roslyn PR for an IDE analyzer:
      1. Please ensure that the analyzer author is made aware of the above doc contribution requirements, especially if it is a community PR.
      2. If the documentation PR in is not being created in parallel to the Roslyn PR, please ensure that a new doc issue is filed in dotnet/docs repo to track this work.
  2. No action required in Roslyn repo: Help links have been hooked up to IDE code style analyzers in Roslyn repo in the base analyzer types, so every new IDE analyzer diagnostic ID will automatically have https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ideXXXX as its help link. There is no action required on the Roslyn PR to ensure this linking.

If you feel any of the existing IDE rule docs have insufficient or incorrect information, please submit a PR to dotnet/docs repo to update the documentation.


Contributing IDE Test Plan (`docs/contributing/IDE Test Plan.md`)

This resource has moved to ../wiki/Manual-Testing.md


Contributing Localization In Compiler Tests (`docs/contributing/Localization In Compiler Tests.md`)

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
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
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.

  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.
    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.

csharp
decimal d = 1.2;
Console.WriteLine(d);

The above code will print 1.2 on en-US but 1,2 on es-ES machines. To fix this issue the value needs to be explicitly formatted with invariant culture.

csharp
using System.Globalization;
...

decimal d = 1.2;
Console.WriteLine(d.ToString(CultureInfo.InvariantCulture));

This will consistently print 1.2.

Exceptions

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.
  2. When a diagnostic includes a message from the underlying OS.
  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
marked is [ConditionalFact(typeof(IsEnglishLocal))] so they run on en-US machines only.

Infrastructure

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.


Contributing Nullable Annotations (`docs/contributing/Nullable Annotations.md`)

Nullable Annotations

This document describes how nullable annotations should be approached in the
Roslyn code base. The default is to simply follow the same guidance
as the dotnet/runtime repository. This document
serves to detail the places where the guidance differs for Roslyn and
re-emphasize rules that come up frequently.

Annotation Guidance

  • DO annotate all new code
  • DO use the null suppression operator in places where a method body has
    ensured a value cannot be null but the compiler is unable to track it.
    Example:
cs
struct SyntaxTrivia {
    void Example() {    
        if (this.HasStructure) {
            Use(this.GetStructure()!);
        }
    }
}
  • DO use explicit validation such as Debug.Assert or
    Contract.ThrowIfNull to capture internal program invariants.
cs
void M1(SyntaxNode node) {
    Debug.Assert(node.Parent is object);
    ...
    M2(node.Parent);
}
  • AVOID annotating and refactoring code in the same commit. This can
    significantly increase the cost of reviewing. Prefer instead to separate
    annotations and significant refactorings into separate commits. Small changes,
    and modernizing style (change default(SyntaxToken) to default) are
    acceptable.
  • DO NOT be concerned with the validity of annotations after an object has
    been disposed or otherwise entered into an invalid state. For example
    manipulating a pooled object after it's been freed. This is a violation of the
    objects contract and annotations aren't meaningful here.
  • DO NOT be concerned with the validity of an enumerator's Current
    annotations when used before MoveNext has been called or after MoveNext has
    returned false. As with Dispose, such use is considered invalid, and we don't
    want to harm the correct consumption of Current by catering to incorrect
    consumption.
  • DO use the adjective Require when defining a member that exist only
    to provide a non-null returning version of a method that returns a nullable
    reference type. For example: GetRequiredSemanticModel model returns a non-null
    value from GetSemanticModel

Breaking Change Guidance

Roslyn is a large code base and it will take significant time for us to fully
annotate the code base. This will likely take several releases for our larger
libraries to complete. During this time those libraries will not consider
nullable annotations to be a part of the compatibility bar. That is
nullable annotations will change without an entry in the breaking change
document, no compat review, etc ...

Once the nullable annotations in a given library reach critical mass we will
begin tracking nullable annotations as a part of our compatibility bar. The
Roslyn team still reserves the right to change annotations if the language or
underlying platforms we build on effectively require us to do so. However such
changes will have a higher degree of scrutiny and will need approval from
members of the API compat council.

As specific libraries complete their nullable annotations then the annotations
will be tracked in API files
and the library names will be added to this document as being complete.


Contributing Powershell Guidelines (`docs/contributing/Powershell Guidelines.md`)

Powershell Guidelines

Powershell is primarily used in this repo as the backbone of our infrastructure. As such
Powershell scripts need to be portable, reliable and take advantage of stop on error
approaches. The guidelines below are meant to push scripts to this mindset of execution.

Coding Style

  1. Opening braces always go at the end of an expression / statement.
  2. Closing braces always occur on an otherwise empty line.
  3. We use two space indentation.
  4. Use Pascal casing for functions and where possible follow the Verb-Name convention.
  5. Use Camel casing for all other identifier.
  6. Use full command names instead of aliases. For example Get-Content vs. gc. Aliases can be
    overridden by the environment and hence are not considered portable.

General Guidelines

Script Template

All Powershell scripts that execute in CI need to conform to the following template.

powershell
[CmdletBinding(PositionalBinding=$false)]
param (
    [switch]$ci = $false,
    [string]$configuration = "Debug")

Set-StrictMode -version 2.0
$ErrorActionPreference="Stop"

try { 
  . (Join-Path $PSScriptRoot "build-utils.ps1")
  $prepareMachine = $ci

  # Body of Powershell script goes here

  ExitWithExitCode 0
}
catch {
  Write-Host $_
  Write-Host $_.Exception
  Write-Host $_.ScriptStackTrace
  ExitWithExitCode 1
}

The rationale for these parts are:

  • Set-StrictMode: forces Powershell into a stricter mode of interpretation and
    swaps out the "On Error Resume Next" approach for an "On Error Stop" model. Both of these
    help with our goals of reliability as it makes errors hard stops (unless specifically stated otherwise)
  • $prepareMachine = $ci: is necessary to ensure ExitWithExitCode properly exits all
    build processes in CI environments (conforming to arcade guidelines).
  • ExitWithExitCode: arcade standard for exiting a script that deals with process management
  • PositionalBinding=$false: alerts callers when they have incorrect parameters invoking a
    script

Coding Guidelines

Use Exec-* functions to execute programs or dotnet commands. This adds automatic
error detection on invocation failure, incorrect parameters, etc ...

powershell
# DO NOT
& msbuild /v:m /m Roslyn.slnx
& dotnet build Roslyn.slnx

# DO
Exec-Command "msbuild" "/v:m /m Roslyn.slnx"
Exec-DotNet "build Roslyn.slnx"

Scripts that have many executions of dotnet commands can store the dotnet command in a variable
and use Exec-Command instead.

Call Test-LastExitCode after invoking a powershell script to make sure failure is not ignored.

powershell
# DO NOT
& eng/make-bootstrap.ps1
Write-Host "Done with Bootstrap"

# DO
& eng/make-bootstrap.ps1
Test-LastExitCode
Write-Host "Done with Bootstrap"

Whenever comparing with $null always make sure to put $null on the left hand side of the
operator. For non-collection types this doesn't really affect behavior. For collection types though
having a collection on the left hand side changes the meaning of -ne and -eq. Instead of checking for $null it will instead compare collection contents.

powershell
# DO NOT
if ($e -ne $null) { ... }
# DO
if ($null -ne $e) { ... }

Powershell vs. pwsh

The Roslyn infrastructure should use powershell for execution not pwsh. The general .NET infra
still uses Powershell and calls into our scripts. Moving to pwsh in our scripts creates errors
in source and unified build. Until that moves to pwsh our scripts need to stay on Powershell.

pwsh is permitted in cases which are not part of our infrastructure. i.e., scripts that are not under the eng/ folder, and not expected to be run by the build process.
This is necessary in scenarios where the script needs to run cross-platform, such as:

  • VS Code helper scripts
  • AI skills

Supporting .cmd file

Consider adding a .cmd file that calls the .ps1 file. This is to support users who are not
running a powershell shell. It also helps with ensuring the script is run locally the same it is
run in CI by controlling the powershell used to invoke the script.

test.cmd

cmd
@echo off
set PSMODULEPATH=
powershell -noprofile -ExecutionPolicy Unrestricted -file "%~dp0\test.ps1" %*

Note: the PSMODULEPATH is cleared to ensure a pwsh shell does not interfere
with launching powershell.


Contributing Testing For Interactive Readiness (`docs/contributing/Testing for Interactive readiness.md`)

Compiler debugging/EnC checklist

  1. Sequence points are emitted correctly and corresponding syntax nodes are recognized as breakpoint spans by the IDE
    • Verify manually by launching VS with the new compiler bits and step through and place breakpoints on all syntax nodes that should allow breakpoint
    • Add regression compiler tests that check emitted IL with sequence points (VerifyIL with sequencePoints)
    • Add IDE test via src\EditorFeatures\CSharpTest\EditAndContinue\BreakpointSpansTests.cs (implementation src\Features\CSharp\Portable\EditAndContinue\BreakpointSpans.cs, which deals with mapping syntax to sequence points)
  2. Sequence points in relationship with closure allocations and other hidden code (for any syntax that produces sequence points)
    • The debugger supports manually moving the current IP (instruction pointer) using “Set Next Statement Ctlr+Shift+F10” command.
    • The statement can be set to any sequence point in the current method.
    • Need to make sure that sequence points are emitted so that the right hidden code gets executed.
    • For example, the sequence point on an opening brace of a block that allocates a closure needs to precede the closure allocation instructions, so that when the IP is set to this sequence point the closure is properly allocated.
text
{               // closure allocated here
   var x = 1;
   F(() => x);
}
  1. Conditional branching has to use stloc/ldloc pattern in DEBUG build
    • Check the instructions and sequence points emitted for e.g. an if statement in debug build. Instead of straightforward brtrue/brfalse we allocate a temp local, store the result of the condition evaluation to that local, emit hidden sequence point, load the result from the local and then branch. This supports EnC function remapping. Only need to think about this when implementing a feature that emits conditional branches with conditions that may contain arbitrary expressions. I believe there are helpers in the lowering phase that emit conditions in general, so it should be done automatically as long as the right helpers are used. But something to be aware of.
  2. Closure and lambda scopes (PDB info)
    • If new syntax is introduced that represents some kind of lambda (anonymous methods, local functions, LINQ queries, etc.) update helpers in src\Compilers\CSharp\Portable\Syntax\LambdaUtilities.cs accordingly
    • If a new scope is introduce that can declare variables that can be lifted into a closure
      • The bound node that represents the scope needs to be associated with syntax node recognized by helpers in src\Compilers\CSharp\Portable\Syntax\LambdaUtilities.cs (specifically IsClosureScope).
      • This requirement is enforced by an assertion in SynthesizedClosureEnvironment constructor.
    • Lambda and closure syntax offsets must be emitted to the PDB (encLambdaMap custom debug information)
      • The offset attribute of closure identifies the syntax node that's associated with the closure. This offset must be unique.
  3. When a new symbol is introduced symbol matcher might need to be updated
    • Symbol matcher maps a symbol from one compilation to another.
    • Synthesized symbols like closures, state machines, anonymous types, lambdas, etc. also has to be mapped.
    • Impl: src\Compilers\CSharp\Portable\Emitter\EditAndContinue\CSharpSymbolMatcher.cs
    • Tests: src\Compilers\CSharp\Test\Emit\Emit\EditAndContinue\SymbolMatcherTests.cs
  4. When a new syntax is introduced that may declare a user local variable or emits long lived synthesized variables (ie. state that survives between breakpoints, not just a temp within expression)
    • Validate that the variable slots can be mapped from new to previous compilation
    • This is implemented by EncVariableSlotAllocator using syntax offsets stored in PDB (encLocalSlotMap and encLambdaMap custom debug info).
    • The current mechanism might not be sufficient to support the mapping, in which case raise the issue with the IDE team to design additional PDB info to support the mapping.
  5. Each new language feature should be covered by a test in Emit tests under Emit/EditAndContinue.
    • Some features might just need a single test others multiple tests depending on impact on EnC.
    • PDB tests validate these scopes. (look for <scope> in PDB XML). LocalsTests.cs EE tests also validate the scoping.
  6. When a new syntax is introduced that may declare a scope for local variables the corresponding IL scopes need to be emitted correctly in the PDB
    • These are used by the EE to determine which variables are in scope.
  7. Test debugging experience of the feature
    • Is useful info displayed in Watch window?
    • Can I evaluate expressions using this feature in Watch window?
    • Some features might require adding more custom PDB information to make the debug experience good (e.g. async, iterators, dynamic, etc).
    • Design experience improvement and custom PDB info with IDE team.

Contributing Bootstrap Builds (`docs/contributing/bootstrap-builds.md`)

Bootstrap Builds

The build correctness leg ensures that the latest code in dotnet/roslyn can be used to build dotnet/roslyn. Essentially our compiler can bootstrap itself.

Why?

The reason we go through so much effort is failures of this nature are incredibly hard to track down after the fact. The compiler is a complex tool and dotnet/roslyn is a large code base. It can take significant time to work out which of the ~300 commits since the last successful bootstrap is causing the current one to fail.

In the past we used to run a bootstrap exercise roughly once a month. Every time there was a failure to build dotnet/roslyn with the latest compiler and it often took weeks for us to understand what change caused the failure. That resulted in a lot of wasted time and almost caused us to slip several releases.

The build correctness jobs was introduced to solve this problem. The compiler verifies it can bootstrap on every change which means we find the failures immediately.

Process

The build correctness job works by first building the Microsoft.Net.Compilers.Toolset package. This gives us a functioning compiler with the latest changes. This build occurs using the /define:BOOTSTRAP which allows the compiler to make failures more actionable. This is primarily leveraged in the following ways:

  • Inserts a ExitingTraceListener into the process trace listeners. This means any Debug.Assert failure will result in the compilation failing with an actionable stack trace.
  • Defines a ValidateBootstrap. This lets us validate that the compiler used in the bootstrap build is actually the one we built vs. the default. This helps protect against build authoring changes which could inadvertently cause the default compiler to be used in a bootstrap build.

The job then cleans out all of the artifacts from the build and starts a normal build of Roslyn.slnx but specifies /p:BootstrapBuildPath=.... This causes two files to be loaded:

This leg also ensures that the binary log and the build server log are captured in the set of published artifacts to allow for easy investigations.

Investigating

The first step for investigating a correctness build failure is downloading the log files. These are available in the published artifacts

The two most interesting files are:

  1. Build.Server.log: this is the text log of the compilation process. All stack traces and server error messages will be present in this file
  2. Build.binlog: this is the binary log that results from building dotnet/roslyn with a bootstrap compiler.

The build server log file will contain the reason why the particular request to the compiler failed. In most cases searching for one of two terms will take you straight to the failure.

The first term to search for is "Debug.Assert" (no quotes). The most common cause of a bootstrap failure is a Debug.Assert call failing during compilation. This will result in an exception being added to the log with the full stack trace. For example:

text
/* Detailed source-code truncated for AI context efficiency. */

These failures are almost always due to changes being tested. Essentially the change updated compiler logic in such a way that it caused an Assert to fail. On occasion this will also fail because an IDE change introduces a coding pattern that sets of a latent bug in the compiler or analyzer but this is certainly the rare case.

The next term to search for is "Error " (no quotes but keep the space). This will be added to the log every the server hits an error and needs to shut down.

txt
ID=MSBuild 60300 TID=3: Error Error: 'EndOfStreamException' 'Reached end of stream before end of read.' occurred during 'Reading response for d2c3aeac-bd8a-4251-bde0-2e11bbc57d13'
Stack trace:
   at Microsoft.CodeAnalysis.CommandLine.BuildProtocolConstants.<ReadAllAsync>d__4.MoveNext() in C:\Users\jaredpar\code\wt\ros2\src\Compilers\Core\CommandLine\BuildProtocol.cs:line 641
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Runtime.CompilerServices.ConfiguredTaskAwaitable.ConfiguredTaskAwaiter.GetResult()
   at Microsoft.CodeAnalysis.CommandLine.BuildResponse.<ReadAsync>d__5.MoveNext() in C:\Users\jaredpar\code\wt\ros2\src\Compilers\Core\CommandLine\BuildProtocol.cs:line 342
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Runtime.CompilerServices.ConfiguredTaskAwaitable`1.ConfiguredTaskAwaiter.GetResult()
   at Microsoft.CodeAnalysis.CommandLine.BuildServerConnection.<<RunServerBuildRequestAsync>g__tryRunRequestAsync|7_1>d.MoveNext() in C:\Users\jaredpar\code\wt\ros2\src\Compilers\Shared\BuildServerConnection.cs:line 288

These type of errors, when not paired with a Debug.Assert failure, are almost always bugs in the compiler server. Please contact the compiler team to help track down such failures.

Note: when you encounter a case where the log file does not have an actionable description of why a build failed, strongly consider sending a PR that fixes this. This approach is why the log is such a valuable tool for tracking down bootstrap failures.

Reproducing locally

Locally reproducing and attaching a debugger to a bootstrap build failure is fairly simple with use of src/Tools/Replay, which can replay a binlog with the checked-out Roslyn compiler source. To do this, run a build locally with the -bl flag to get a binary log, then use replay to replay the binary log:

Windows

powershell
.\build.ps1 -bl
dotnet run --framework net10.0 --project src\Tools\Replay\ artifacts\log\Debug\Build.binlog -w

Linux

sh
./build.sh -bl
dotnet run --framework net10.0 --project src/Tools/Replay/ artifacts/log/Debug/Build.binlog -w

The -w flag will cause replay to print the process ID of the compiler and wait until you hit a key, so that you can attach a debugger to the process and set breakpoints for exceptions you are interested in.

Debugging

To debug a bootstrap build failure locally do the following.

The first step is disabling the ExitingTraceListener. This is important for CI where the compiler needs to crash on a Debug.Assert failure vs. popping up a dialog that would hang CI. When debugging locally though developers want the Debug.Assert pops up a dialog behavior. To disable the ExitingTraceListener comment out the following line:

https://github.com/dotnet/roslyn/blob/d73d31cbccb9aa850f3582afb464b709fef88fd7/src/Compilers/Server/VBCSCompiler/VBCSCompiler.cs#L22

Next just run the bootstrap build locally, wait for the Debug.Assert to trigger which pops up a dialog. From there you can attach to the VBCSCompiler process and debug through the problem

cmd
> Build.cmd -bootstrap

Contributing Codespaces (`docs/contributing/codespaces.md`)

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 <solution> 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.

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.

Contributing Ide Design Process (`docs/contributing/ide_design_process.md`)

IDE Team Design Process

Discussions / Issues

The goal is that contributors can create and discuss feature suggestions without needing to have everything fully figured out. Feel free to file a new Github issue or start a discussion. These conversations can continue at their own pace until contributors feel ready to make a more formal proposal.

Proposals

Adding a proposal

For a feature proposal to be reviewed, a Github issue needs to be created that has the following items in its description. If these aren't there to start they eventually need to be appended as a comment to the issue before its brought up for discussion.

  • Summary: Brief summary of what this proposal is about.
  • Background and Motivation: What is the problem we are solving and in what context did you encounter it?
  • Proposed Feature: How the proposed solution solves the problem?
  • Alternative Designs: Were there other options you considered? How does this compare to features in other editors?

Once you've added the details for your feature tag a member of the Roslyn-IDE team and they will add the "Feature Request" label to your Github issue.

Getting the issue championed by the IDE team

A member of Roslyn-IDE will need to decide to "champion" that feature proposal and bring it for discussion by adding it to the "On Deck" tab in the IDE Design Review project. Behind the scenes the IDE Team generally meets once a week to discuss these issues.


Contributing Target Framework Strategy (`docs/contributing/target-framework-strategy.md`)

Target Framework Strategy

Layers

The roslyn repository produces components for a number of different products that push varying ship and TFM constraints on us. A summary of some of our dependencies are:

  • Build Tools: requires us to ship compilers on net472
  • .NET SDK: requires us to ship compilers on current servicing target framework (presently net10.0)
  • Repository Source build: requires us to ship $(NetCurrent) and $(NetPrevious) in workspaces and below (presently net10.0 and net9.0 respectively). This is because the output of repository source build is an input to other repository source build and those could be targeting either $(NetCurrent) or $(NetPrevious).
  • Full Source build: requires us to ship $(NetCurrent)
  • Visual Studio: requires us to ship net472 for base IDE components and $(NetVisualStudio) (presently net10.0) for private runtime components.
  • Visual Studio Code: expects us to ship against the same runtime as DevKit (presently net10.0) to avoid two runtime downloads.
  • MSBuildWorkspace: requires us to ship a process that must be usable on the lowest supported SDK (presently net8.0)
  • Microsoft.RoslynTools: remains compatible with the lowest runtime supported by Roslyn infrastructure automation (presently net8.0).

It is not reasonable for us to take the union of all TFM and multi-target every single project to them. That would add several hundred compilations to any build operation which would in turn negatively impact our developer throughput. Instead we attempt to use the TFM where needed. That keeps our builds smaller but increases complexity a bit as we end up shipping a mix of TFM for binaries across our layers.

Picking the right TargetFramework

Projects in our repository should include the following values in <TargetFramework(s)> based on the rules below:

  1. $(NetRoslynSourceBuild): code that needs to be part of source build. This property will change based on whether the code is building in a source build context or official builds.
    a. In official builds this will include the TFMs for $(NetVSShared)
    b. In source builds this will include $(NetRoslyn)
  2. $(NetVS): code that needs to execute on the private runtime of Visual Studio.
  3. $(NetVSCode): code that needs to execute in DevKit host
  4. $(NetVSShared): code that needs to execute in both Visual Studio and VS Code but does not need to be source built.
  5. $(NetRoslyn): code that needs to execute on .NET but does not have any specific product deployment requirements. For example utilities that are used by our infra, compiler unit tests, etc ... This property also controls which of the frameworks the compiler builds against are shipped in the toolset packages. This value will potentially change in source builds.
  6. $(NetRoslynAll): code, generally test utilities, that need to build for all .NET runtimes that we support. Note: $(NetRoslynBuildHostNetCoreVersion) is excluded from this set because the BuildHost is an isolated executable.
  7. $(NetRoslynWindowsTests): test projects that use $(NetRoslyn) but can only run on Windows. These tests will be automatically excluded when running on Unix/Linux because the -windows OS-specific TFM suffix tells the .NET SDK and the test runner to skip them on non-Windows platforms.
  8. $(NetRoslynBuildHostNetCoreVersion): the target used for the .NET Core BuildHost process used by MSBuildWorkspace. This may lag behind other properties as it must target the lowest supported .NET SDK (presently net8.0 until .NET 8 EOL in November 2026).
  9. $(NetRoslynNext): code that needs to run on the next .NET Core version. This is used during the transition to a new .NET Core version where we need to move forward but don't want to hard code a .NET Core TFM into the build files.
  10. $(NetRoslynTools): the target used by the Microsoft.RoslynTools infrastructure tool. This may lag behind $(NetRoslyn) so build and insertion automation can run the tool on older supported machines.

This properties $(NetCurrent), $(NetPrevious) and $(NetMinimum) are not used in our project files because they change in ways that make it hard for us to maintain correct product deployments. Our product ships on VS and VS Code which are not captured by arcade $(Net...) macros. Further as the arcade properties change it's very easy for us to end up with duplicate entries in a <TargetFrameworks> setting. Instead our repo uses the above values and when inside source build or VMR our properties are initialized with arcade properties.

DO NOT hard code .NET Core TFMs in project files. Instead use the properties above as that lets us centrally manage them and structure the properties to avoid duplication. It is fine to hard code other TFMs like netstandard2.0 or net472 as those are not expected to change.

DO NOT use $(NetCurrent) or $(NetPrevious) in project files. These should only be used inside of TargetFrameworks.props to initialize the above values in certain configurations.

Require consistent API across Target Frameworks

It is important that our shipping APIs maintain consistent API surface area across target frameworks. That is true whether the API is public or internal.

The reason for public is standard design pattern. The reason for internal is a combination of the following problems:

  • Our repository makes use of InternalsVisibleTo which allows other assemblies to directly reference signatures of internal members.
  • Our repository ships a mix of target frameworks. Typically workspaces and below will ship more recent TFMs than the layers above it. Compiler has to ship newer TFM for source build while IDE is constrained by Visual Studio's private runtime hence adopts newer TFM slower.
  • Our repository invests in polyfill APIs to make compiling against multiple TFMs in the same project a seamless experience.

Taken together though this means that our internal surface area in many cases is effectively public when it comes to binary compatibility. For example a consuming project can end up with net7.0 binaries from workspaces layer and net6.0 binaries from IDE layer. Because there is InternalsVisibleTo between these binaries the internal API surface area is effectively public. This requires us to have a consistent strategy for achieving binary compatibility across TFM combinations.

Consider a specific example of what goes wrong when our internal APIs are not consistent across TFM:

  • Workspaces today targets net6.0 and net7.0 and it contains our EnumerableExtensions.cs which polyfills many extensions methods on IEnumerable<T>. In net7.0 the Order extension method is not needed because it was put into the .NET core libraries.
  • Language Server Protocol targets net6.0 and consumes the Order polyfill from Workspaces

Let's assume for a second that we #if the Order method such that it's not present in net7.0. Locally this all builds because we compile the net6.0 versions against each other so they're consistent. However if an external project which targets net7.0 and consumes both Workspaces and Language Server Protocol then it will be in a broken state. The Protocol binary is expecting Workspaces to contain a polyfill method for Order but it does not since it's at net7.0 and it was #if out. As a result this will fail at runtime with missing method exceptions.

This problem primarily comes from our use of polyfill APIs. To avoid this we employ the following rule:

When there is a #if directive that matches the regex #if !?NET.* that declares a non-private member, there must be an #else that defines an equivalent binary compatible symbol

This comes up in two forms:

Pattern for types

When creating a polyfill for a type use the #if !NET... to declare the type and in the #else use a TypeForwardedTo for the actual type.

Example:

csharp
#if NET6_0_OR_GREATER

using System.Runtime.CompilerServices;

#pragma warning disable RS0016 // Add public types and members to the declared API (this is a supporting forwarder for an internal polyfill API)
[assembly: TypeForwardedTo(typeof(IsExternalInit))]
#pragma warning restore RS0016 // Add public types and members to the declared API

#else

using System.ComponentModel;

namespace System.Runtime.CompilerServices
{
    internal static class IsExternalInit
    {
    }
}

#endif

Pattern for extension methods

When creating a polyfill for an extension use the #if NET... to declare the extension method and the #else to declare the same method without this. That will put a method with the expected signature in the binary but avoids it appearing as an extension method within that target framework.

csharp
#if NET7_0_OR_GREATER
        public static IOrderedEnumerable<T> Order<T>(IEnumerable<T> source) where T : IComparable<T>
#else
        public static IOrderedEnumerable<T> Order<T>(this IEnumerable<T> source) where T : IComparable<T>
#endif

Transitioning to new .NET SDK

As the .NET team approaches releasing a new .NET SDK the Roslyn team will begin using preview versions of that SDK in our build. This will often lead to test failures in our CI system due to underlying behavior changes in the runtime. These failures will often not show up when running in Visual Studio due to the way the runtime for the test runner is chosen.

To ensure we have a simple developer environment such project should be moved to to the $(NetRoslynNext) target framework. That ensures the new runtime is loaded when running tests locally.

When the .NET SDK RTMs and Roslyn adopts it all occurrences of $(NetRoslynNext) will be moved to simply $(NetRoslyn).

DO NOT include both $(NetRoslyn) and $(NetRoslynNext) in the same project unless there is a very specific reason that both tests are adding value. The most common case is that the runtime has changed behavior and we simply need to update our baselines to match it. Adding extra TFMs for this just increases test time for very little gain.

Checklist for updating TFMs (once a year)

  • Update TargetFrameworks.props.
    • Ensure we have the correct TFM for VS / VSCode (usually not the latest TFM).
  • Might need updating MicrosoftNetCompilersToolsetVersion in eng\Versions.props to consume latest compiler features.
  • Change $(NetRoslynNext) references to $(NetRoslyn) in project files.
  • Update TFMs in code/scripts/pipelines (search for the old netX.0 and replace with the new netY.0).
  • Replace #if NET7_0_OR_GREATER corresponding to the no longer used TFM (net7.0 in this example) with the lowest used TFM (e.g., net8.0: #if NET8_0_OR_GREATER) and similarly the negated variant #if !NET7_0_OR_GREATER with #if !NET8_0_OR_GREATER. See https://github.com/dotnet/roslyn/issues/75453#issuecomment-2405500584.
  • Check that the same number of tests still run in CI (they are not unintentionally filtered out by TFM).
  • Try an official build, a VS insertion.

Features CheckedUserDefinedOperators (`docs/features/CheckedUserDefinedOperators.md`)

Checked user-defined operators

C# should support defining checked variants of the following user-defined operators so that users can opt into or out of overflow behavior as appropriate:

Proposal:

Feature branch: https://github.com/dotnet/roslyn/tree/features/CheckedUserDefinedOperators

Test plan: https://github.com/dotnet/roslyn/issues/59458


Features DefaultInterfaceImplementation (`docs/features/DefaultInterfaceImplementation.md`)

Default Interface Implementation

The Default Interface Implementation feature enables a default implementation of an interface member to be provided as part of the interface declaration.

Here is a link to the proposal https://github.com/dotnet/csharplang/blob/main/proposals/csharp-8.0/default-interface-methods.md.

What is supported:

  • Supplying an implementation along with declaration of a regular interface method and recognizing that implementation as default implementation for the method when a type implements the interface.
    Here is an example:
text
public interface I1
{
    void M1() 
    {
        System.Console.WriteLine("Default implementation of M1 is called!!!");
    }
}

class Test1 : I1
{
    static void Main()
    {
        I1 x = new Test1();
        x.M1();
    }
}
  • Re-abstraction of interface implementation in derived interfaces. Types implementing the derived interface are required to supply implementation even when a base interface provides an implementation.
    Here is an example:
text
public interface I1
{
    void M1() 
    {
    }

    int P1 
    {
        get => throw null;
        set => throw null;
    }

    event System.Action E1
    {
        add => throw null;
        remove => throw null;
    }
}

public interface I2 : I1
{
    abstract void I1.M1();
    abstract int I1.P1 {get;set;}
    abstract event System.Action I1.E1;
}

class Test1 : I2 // This type must implement all members of I1
{
}

In metadata, methods representing re-abstraction have all three flags abstract, virtual, sealed set and do not have newslot flag set.

  • Supplying an implementation along with declaration of a property or an indexer and recognizing that implementation as default implementation for them when a type implements the interface.

  • Supplying an implementation along with declaration of an event and recognizing that implementation as default implementation for the event when a type implements the interface.

  • Using partial, public, internal, private, protected, static, virtual, sealed, abstract, extern and async modifiers with interface methods.

  • Using public, internal, private, protected, static, virtual, sealed, abstract and extern modifiers with interface properties.

  • Using public, internal, private, protected, virtual, sealed, abstract and extern modifiers with interface indexers.

  • Using internal, private, protected modifiers with interface property/indexer accessors.

  • Using public, internal, private, protected, static, virtual, sealed, abstract and extern modifiers with interface events.

  • Declaring types within interfaces.

  • Implementing interface methods in derived interfaces by using explicit implementation syntax, accessibility is protected, allowed modifiers: extern and async.

  • Implementing interface properties and indexers in derived interfaces by using explicit implementation syntax, accessibility protected, allowed modifiers: extern.

  • Implementing interface events in derived interfaces by using explicit implementation syntax, accessibility protected, no allowed modifiers.

  • Declaring static fields, auto-properties and field-like events.

  • Declaring operators + - ! ~ ++ -- true false * / % & | ^ << >> > < >= <= in interfaces.

  • Base access
    The following forms of base-access are added (https://github.com/dotnet/csharplang/blob/main/meetings/2018/LDM-2018-11-14.md)

text
base ( <type-syntax> )  .   identifier
    base ( <type-syntax> )   [   argument-list   ]

The type-syntax can refer to one of the base classes of the containing type, or one of the interfaces implemented or inherited by the containing type.

When the type-syntax refers to a class, the member lookup rules, overload resolution rules and IL emit match the rules for the 7.3 supported
forms of base-access. The difference is that the specified base class is used instead of the immediate base class. The most derived implementation
found must be a member of that class.

When the type-syntax refers to an interface:

  1. The member lookup is performed in that interface, using the regular member lookup rules within interfaces, with an exception that members of
    System.Object do not participate in the lookup.
  2. Regular overload resolution is performed for members returned by the lookup process, virtual or abstract members are not replaced with most
    derived implementations at this step (unlike the case when the type-syntax refers to a class). If result of overload resolution is a virtual
    or abstract method, it must have an implementation within the specified interface type, an error is reported otherwise. That
    implementation must be accessible at the call site. If result of overload resolution is a non-virtual method, the method must be declared in the
    specified interface type.
  3. During IL emit a call (non-virtual call) instruction is used to invoke methods. If result of overload resolution on the previous step is
    a virtual or abstract method, the implementation of the method from the specified interface is used as the target for the instruction.

Given the accessibility requirements for the most specific interface implementation, accessibility of implementations provided in derived interfaces
is changed to protected.

Open issues and work items are tracked in https://github.com/dotnet/roslyn/issues/17952.

Parts of ECMA-335 that become obsolete/inaccurate/incomplete

I.8.5.3.2 Accessibility of members and nested types
Members (other than nested types) defined by an interface shall be public.
I.8.9.4 Interface type definition
Similarly, an interface type definition shall not provide implementations for any methods on the
values of its type.
Interfaces can have static or virtual methods, but shall not have instance methods.
However, since accessibility attributes are relative to the implementing type rather
than the interface itself, all members of an interface shall have public accessibility, ...
I.8.11.1 Method definitions
All non-static methods of an interface definition are abstract.
All non-static method definitions in interface definitions shall be virtual methods.
II.10.4 Method implementat ion requirements
II.12 Semantics of interfaces
Interfaces can have static fields and methods, but they shall not have instance fields or
methods. Interfaces can define virtual methods, but only if those methods are abstract
(see Partition I and §II.15.4.2.4).
II.12.2 Implement ing virtual methods on interfaces
If the class defines any public virtual methods whose name and signature
match a virtual method on the interface, then add these to the list for that
method, in type declaration order (see above).
If there are any public virtual methods available on this class (directly or inherited)
having the same name and signature as the interface method, and whose generic type
parameters do not exactly match any methods in the existing list for that interface
method for this class or any class in its inheritance chain, then add them (in type
declaration order) to the list for the corresponding methods on the interface.
II.15.2 Static, instance, and virtual methods
It follows that instance methods shall only be defined in classes or value types,
but not in interfaces or outside of a type (i.e., globally).
II.22.27 MethodImpl : 0x19
The method indexed by MethodBody shall be a member of Class or some base class
of Class (MethodImpls do not allow compilers to ‘hook’ arbitrary method bodies)
II.22.37 TypeDef : 0x02
All of the methods owned by an Interface (Flags.Interface = 1) shall be abstract
(Flags.Abstract = 1)
IV.6 Implementation-specific modifications to the system libraries
Interfaces and virtual methods shall not be added to an existing interface.


Features ExperimentalAttribute (`docs/features/ExperimentalAttribute.md`)

ExperimentalAttribute

Report warnings for references to types and members marked with Windows.Foundation.Metadata.ExperimentalAttribute.

text
namespace Windows.Foundation.Metadata
{
    [AttributeUsage(
        AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Enum |
        AttributeTargets.Interface | AttributeTargets.Delegate,
        AllowMultiple = false)]
    public sealed class ExperimentalAttribute : Attribute
    {
    }
}

Warnings

The warning message is a specific message, where '{0}' is the fully-qualified type or member name.

text
'{0}' is for evaluation purposes only and is subject to change or removal in future updates.

The warning is reported for any reference to a type or member marked with the attribute.
(The AttributeUsage is for types only so the attribute cannot be applied to non-type members from C# or VB,
but the attribute could be applied to non-type members outside of C# or VB.)

text
[Experimental]
class A
{
    internal object F;
    internal static object G;
    internal class B { }
}
class C
{
    static void Main()
    {
        F(default(A));   // warning CS08305: 'A' is for evaluation purposes only ...
        F(typeof(A));    // warning CS08305: 'A' is for evaluation purposes only ...
        F(nameof(A));    // warning CS08305: 'A' is for evaluation purposes only ...
        var a = new A(); // warning CS08305: 'A' is for evaluation purposes only ...
        F(a.F);
        F(A.G);          // warning CS08305: 'A' is for evaluation purposes only ...
        F<A.B>(null);    // warning CS08305: 'A' is for evaluation purposes only ...
    }
    static void F<T>(T t)
    {
    }
}

The attribute is not inherited from base types or overridden members.

text
[Experimental]
class A<T>
{
    [Experimental]
    internal virtual void F()
    {
    }
}
class B : A<int> // warning CS08305: 'A<int>' is for evaluation purposes only ...
{
    internal override void F()
    {
        base.F(); // warning CS08305: 'A<int>.F' is for evaluation purposes only ...
    }
}
class C
{
    static void F(A<object> a, B b) // warning CS08305: 'A<object>' is for evaluation purposes only ...
    {
        a.F(); // warning CS08305: 'A<object>.F' is for evaluation purposes only ...
        b.F();
    }
}

There is no automatic suppression of warnings: warnings are generated for all references, even within [Experimental] members.
Suppressing the warning requires an explicit compiler option or #pragma.

text
[Experimental] enum E { }
[Experimental]
class C
{
    private C(E e) // warning CS08305: 'E' is for evaluation purposes only ...
    {
    }
    internal static C Create() // warning CS08305: 'C' is for evaluation purposes only ...
    {
        return Create(0);
    }
#pragma warning disable 8305
    internal static C Create(E e)
    {
        return new C(e);
    }
}

ObsoleteAttribute and DeprecatedAttribute

ExperimentalAttribute is independent of System.ObsoleteAttribute or Windows.Framework.Metadata.DeprecatedAttribute.

Warnings for [Experimental] are reported within [Obsolete] or [Deprecated] members.
Warnings and errors for [Obsolete] and [Deprecated] are reported inside [Experimental] members.

text
[Obsolete]
class A
{
    static object F() => new C(); // warning CS08305: 'C' is for evaluation purposes only ...
}
[Deprecated(null, DeprecationType.Deprecate, 0)]
class B
{
    static object F() => new C(); // warning CS08305: 'C' is for evaluation purposes only ...
}
[Experimental]
class C
{
    static object F() => new B(); // warning CS0612: 'B' is obsolete
}

Warnings and errors for [Obsolete] and [Deprecated] are reported instead of [Experimental] if there are multiple attributes.

text
[Obsolete]
[Experimental]
class A
{
}
[Experimental]
[Deprecated(null, DeprecationType.Deprecate, 0)]
class B
{
}
class C
{
    static A F() => null; // warning CS0612: 'A' is obsolete
    static B G() => null; // warning CS0612: 'B' is obsolete
}

Features ExpressionVariables (`docs/features/ExpressionVariables.md`)

Expression Variables

The expression variables feature extends the features introduced in C# 7 to permit expressions
containing expression variables (out variable declarations and declaration patterns) in field
initializers, property initializers, ctor-initializers, and query clauses.

See https://github.com/dotnet/csharplang/issues/32 and
https://github.com/dotnet/csharplang/blob/main/proposals/csharp-7.3/expression-variables-in-initializers.md
for more information.

Current state of the feature:

[X] Permit in field initializers
[X] Permit in property initializers
[ ] Permit in ctor-initializers
[X] Permit in query clauses


Features ExtendedLayoutAttribute (`docs/features/ExtendedLayoutAttribute.md`)

System.Runtime.InteropServices.ExtendedLayoutAttribute Compiler Support

The .NET runtime team is introducing a new attribute, System.Runtime.InteropServices.ExtendedLayoutAttribute, which will allow the runtime team to provide additional layout options for types, primarily for interop scenarios.
To provide the user experience requested by the runtime team, the C# and VB compilers will have the following support for this attribute:

  • If a type has the ExtendedLayoutAttribute applied, the compiler will emit the TypeAttributes.ExtendedLayout value in the type's TypeAttributes flags.
  • If a type has the ExtendedLayoutAttribute applied, the compiler will not allow the StructLayoutAttribute to be applied to the type.
  • (C# compiler only) If a type has the ExtendedLayoutAttribute applied, the compiler will not allow the InlineArrayAttribute to be applied to the type.
  • Within the Roslyn compiler, the ITypeSymbol for a type with the ExtendedLayoutAttribute applied will have the following behavior:
    • The Layout property will return a TypeLayout instance with the LayoutKind set to Extended (1), Size set to 0 and Pack set to 0.
  • A type that is embedded in an assembly (using the NoPia technology) will have the ExtendedLayoutAttribute preserved on the embedded type.

The Roslyn compiler will not have knowledge of the specific options available on the ExtendedLayoutAttribute, as these options will be defined by the runtime team and may expand over time. The compiler will not attempt to detect invalid field types for specific layout options. The runtime team may add analyzers in the future to handle these scenarios.

The compiler will only recognize the presence of the attribute and its implications for the type symbol's LayoutKind and the emitted attribute.


Features GlobalUsingDirective (`docs/features/GlobalUsingDirective.md`)

Global Using Directive

The Global Using Directive feature extends using directive syntax with an optional global keyword that can precede the using keyword.
The scope of Global Using Directives spans across all compilation units in the program.

Proposal: https://github.com/dotnet/csharplang/blob/master/proposals/GlobalUsingDirective.md

Feature branch: https://github.com/dotnet/roslyn/tree/features/GlobalUsingDirective

Test plan: https://github.com/dotnet/roslyn/issues/51307


Features NativeIntegerAttribute (`docs/features/NativeIntegerAttribute.md`)

Native Integer Metadata

The following describes the representation of native integers in metadata.

NativeIntegerAttribute

Type references are annotated in metadata with a NativeIntegerAttribute.

c#
namespace System.Runtime.CompilerServices
{
    [AttributeUsage(
        AttributeTargets.Class |
        AttributeTargets.Event |
        AttributeTargets.Field |
        AttributeTargets.GenericParameter |
        AttributeTargets.Parameter |
        AttributeTargets.Property |
        AttributeTargets.ReturnValue,
        AllowMultiple = false,
        Inherited = false)]
    public sealed class NativeIntegerAttribute : Attribute
    {
        public NativeIntegerAttribute()
        {
            TransformFlags = new[] { true };
        }
        public NativeIntegerAttribute(bool[] flags)
        {
            TransformFlags = flags;
        }
        public readonly bool[] TransformFlags;
    }
}

The NativeIntegerAttribute type is for compiler use only - it is not permitted in source.
The type declaration is synthesized by the compiler if not already included in the compilation.

Each type reference in metadata may have an associated NativeIntegerAttribute with a bool[] where each bool
represents whether the associated type is a native integer (nint or nuint in C#) or the underlying type (System.IntPtr or System.UIntPtr).
Custom modifiers associated with the type reference are ignored.

A type reference may have at most one NativeIntegerAttribute.
The attribute is not inherited.

The bool[] is constructed as follows:

  • System.IntPtr, System.UIntPtr: true if the type represents a native integer; false otherwise
  • Generic type: the representation of the type arguments in order including containing types
  • Tuple: the representation of the underlying constructed type
  • Array: the representation of the element type
  • Pointer type: the representation of the pointed at type
  • Function pointer type: the representation of the return type followed by the parameter types in order
  • Other: none

Given the construction above, the number of elements in the bool[] matches the number of references to System.IntPtr and System.UIntPtr in the type.

Optimizations

If the bool[] is empty, the NativeIntegerAttribute is omitted.

If the bool[] contains a single true value, the NativeIntegerAttribute uses the parameter-less constructor.

Examples

c#
System.IntPtr A;         // System.IntPtr A
nint B;                  // [NativeInteger] System.IntPtr B
IEnumerable<nuint> C     // [NativeInteger] System.Collections.Generic.IEnumerable<System.IntPtr> C
(System.IntPtr, nint) D; // [NativeInteger(new[] { false, true })] System.ValueTuple<System.IntPtr, System.IntPtr> D

Features StaticAbstractMembersInInterfaces (`docs/features/StaticAbstractMembersInInterfaces.md`)

Static Abstract Members In Interfaces

An interface is allowed to specify abstract static members that implementing classes and structs are then
required to provide an explicit or implicit implementation of. The members can be accessed off of type
parameters that are constrained by the interface.

Proposal:

Feature branch: https://github.com/dotnet/roslyn/tree/features/StaticAbstractMembersInInterfaces

Test plan: https://github.com/dotnet/roslyn/issues/52221


Features TopLevelStatements (`docs/features/TopLevelStatements.md`)

Top-level statements

Allow a sequence of statements to occur right before the namespace_member_declarations of a compilation_unit (i.e. source file).

The semantics are that if such a sequence of statements is present, the following type declaration, modulo the actual type name and the method name, would be emitted:

c#
static class Program
{
    static async Task Main()
    {
        // statements
    }
}

Proposal: https://github.com/dotnet/csharplang/blob/main/proposals/top-level-statements.md
Open issues and TODOs are tracked at https://github.com/dotnet/roslyn/issues/41704.
Test plan: https://github.com/dotnet/roslyn/issues/43563.
See also https://github.com/dotnet/csharplang/issues/3117.


Features UsedAssemblyReferences (`docs/features/UsedAssemblyReferences.md`)

Used Assembly References

The Used Assembly References feature provides a GetUsedAssemblyReferences API on a Compilation to obtain a set
of metadata assembly references that are considered to be used by the compilation. For example, if a type declared in a
referenced assembly is referenced in source code within the compilation, the reference is considered to be used. Etc.

See https://github.com/dotnet/roslyn/issues/37768 for more information.


Features Async Main (`docs/features/async-main.md`)

Async Task Main

[dotnet/csharplang proposal](https://github.com/dotnet/csharplang/blob/main/proposals/async-main.md)

Technical Details

  • The compiler must recognize Task and Task as valid entrypoint return types in addition to void and int.
  • The compiler must allow async to be placed on a main method that returns a Task or a Task<T> (but not void).
  • The compiler must generate a shim method $EntrypointMain that mimics the arguments of the user-defined main.
    • static async Task Main(...) -> static void $EntrypointMain(...)
    • static async Task Main(...) -> static int $EntrypointMain(...)
    • The parameters between the user-defined main and the generated main should match exactly.
  • The body of the generated main should be return Main(args...).GetAwaiter().GetResult();

Features Async Main.Test (`docs/features/async-main.test.md`)

Main areas to test:

  • Signature acceptance / rejection
  • Method body creation

Signature acceptance / rejection

Single Mains

Classes that contain only a single "main" method

  • Past: Ok
  • New 7: Ok
  • New 7.1 Ok

Single async (void) main

  • Past: ERR (Async can't be main)
  • New 7: ERR (Update to get this to work)
  • New 7.1: Ok

Single async (Task) main

  • Past: ERR (No entrypoints found), WARN (has the wrong signature to be an entry point)
  • New 7: ERR (Update to get this to work)
  • New 7.1 Ok

Single async (Task) main

  • Past: ERR (No entrypoints found), WARN (has the wrong signature)
  • New 7: ERR (Update to get this to work)
  • New 7.1: Ok

Multiple Mains

Classes that contain more than one main

  • Past: Err
  • New 7: Err
  • New 7.1: Err
  • Past: Err (an entrypoint cannot be marked with async)
  • New 7: Err (new error here?)
  • New 7.1: Err (new error here? "void is not an acceptable async return type")
  • Past: Ok (warning: task main has wrong signature)
  • New 7: Ok (new warning here)
  • New 7.1: Ok (new warning here?)
  • Past: Ok (warning: task main has wrong signature)
  • New 7: Ok (new warning here)
  • New 7.1: Ok (new warning here?)

Method body creation

  • Inspect IL for correct codegen.
  • Make sure that attributes are correctly applied to the synthesized mains.

Broken methods on Task and Task

  • GetAwatier or GetResult are missing
  • GetAwaiter or GetResult don't have the required signature
  • Has the right shape, but types are missing

Task or Task is missing

This will be caught during entrypoint detection, should be a binding error.

Void or int is missing

If Task can be found, but void or int can't be found, then the compiler should behave gracefully.

Vlad Test Plan

Public interface of compiler APIs

-N/A?

General functionality

  • Task and Task returns are allowed. Do we require async.
  • Multiple valid/invalid async candidates.
  • process exit code on success (void and int) and on exception
  • /main:<type> cmd switch is still functional

Old versions, compat

  • langver
  • when both async and regular Main avaialble. With old/new compiler versions.
  • async void Main. With old.new langver. With another applicable Main present.

Type and members

  • Access modifiers (public, protected, internal, protected internal, private), static modifier
  • Parameter modifiers (ref, out, params)
  • STA attribute
  • Partial method
  • Named and optional parameters
  • Task<dynamic>
  • Task-Like types

Debug

  • F11 works
  • stepping through Main

Features Async Streams (`docs/features/async-streams.md`)

async-streams (C# 8.0)

Async-streams are asynchronous variants of enumerables, where getting the next element may involve an asynchronous operation. They are types that implement IAsyncEnumerable<T>.

csharp
// Those interfaces will ship as part of .NET Core 3
namespace System.Collections.Generic
{
    public interface IAsyncEnumerable<out T>
    {
        IAsyncEnumerator<T> GetAsyncEnumerator(CancellationToken token = default);
    }

    public interface IAsyncEnumerator<out T> : System.IAsyncDisposable
    {
        System.Threading.Tasks.ValueTask<bool> MoveNextAsync();
        T Current { get; }
    }
}
namespace System
{
    public interface IAsyncDisposable
    {
        System.Threading.Tasks.ValueTask DisposeAsync();
    }
}

When you have an async-stream, you can enumerate its items using an asynchronous foreach statement: await foreach (var item in asyncStream) { ... }.
An await foreach statement is just like a foreach statement, but it uses IAsyncEnumerable instead of IEnumerable, each iteration evaluates an await MoveNextAsync(), and the disposable of the enumerator is asynchronous.

Similarly, if you have an async-disposable, you can use and dispose it with asynchronous using statement: await using (var resource = asyncDisposable) { ... }
An await using statement is just like a using statement, but it uses IAsyncDisposable instead of IDisposable, and await DisposeAsync() instead of Dispose().

The user can implement those interfaces manually, or can take advantage of the compiler generating a state-machine from a user-defined method (called an "async-iterator" method).
An async-iterator method is a method that:

  1. is declared async,
  2. returns an IAsyncEnumerable<T> or IAsyncEnumerator<T> type,
  3. uses both await syntax (await expression, await foreach or await using statements) and yield statements (yield return, yield break).

For example:

csharp
async IAsyncEnumerable<int> GetValuesFromServer()
{
    while (true)
    {
        IEnumerable<int> batch = await GetNextBatch();
        if (batch == null) yield break;

        foreach (int item in batch)
        {
            yield return item;
        }
    }
}

Just like in iterator methods, there are several restrictions on where a yield statement can appear in async-iterator methods:

  • It is a compile-time error for a yield statement (of either form) to appear in the finally clause of a try statement.
  • It is a compile-time error for a yield return statement to appear anywhere in a try statement that contains any catch clauses.

Detailed design for `await using` statement

An asynchronous using is lowered just like a regular using, except that Dispose() is replaced with await DisposeAsync().

Note that pattern-based lookup for DisposeAsync binds to instance methods that can be invoked without arguments.
Extension methods do not contribute. The result of DisposeAsync must be awaitable.

Detailed design for `await foreach` statement

An await foreach is lowered just like a regular foreach, except that:

  • GetEnumerator() is replaced with await GetAsyncEnumerator()
  • MoveNext() is replaced with await MoveNextAsync()
  • Dispose() is replaced with await DisposeAsync()

Note that pattern-based lookup for GetAsyncEnumerator, MoveNextAsync and DisposeAsync binds to instance methods that can be invoked without arguments.
Extension methods do not contribute. The result of MoveNextAsync and DisposeAsync must be awaitable.
Disposal for await foreach does not include a fallback to a runtime check for an interface implementation.

Asynchronous foreach loops are disallowed on collections of type dynamic,
as there is no asynchronous equivalent of the non-generic IEnumerable interface.

But wrapper types can pass non-default values (see .WithCancellation(CancellationToken) extension method),
thereby allowing consumers of async-streams to control cancellation.
A producer of async-streams can make use of the cancellation token by writing an
IAsyncEnumerator<T> GetAsyncEnumerator(CancellationToken) async-iterator method in a custom type.

csharp
E e = ((C)(x)).GetAsyncEnumerator(default);
try
{
    while (await e.MoveNextAsync())
    {
        V v = (V)(T)e.Current;  -OR-  (D1 d1, ...) = (V)(T)e.Current;
        // body
    }
}
finally
{
    await e.DisposeAsync();
}

Detailed design for async-iterator methods

An async-iterator method is replaced by a kick-off method, which initializes a state machine. It does not start running the state machine (unlike kick-off methods for regular async method).
The kick-off method method is marked with AsyncIteratorStateMachineAttribute.

The state machine for an enumerable async-iterator method primarily implements IAsyncEnumerable<T> and IAsyncEnumerator<T>.
For an enumerator async-iterator, it only implements IAsyncEnumerator<T>.
It is similar to a state machine produced for an async method.
It contains builder and awaiter fields, used to run the state machine in the background (when an await is reached in the async-iterator).
It also captures parameter values (if any) or this (if needed).

But it contains additional state:

  • a promise of a value-or-end,
  • a current yielded value of type T,
  • an int capturing the id of the thread that created it,
  • a bool flag indicating "dispose mode",
  • a CancellationTokenSource for combining tokens (in enumerables).

The central method of the state machine is MoveNext(). It gets run by MoveNextAsync(), or as a background continuation initiated from these from an await in the method.

The promise of a value-or-end is returned from MoveNextAsync. It can be fulfilled with either:

  • true (when a value becomes available following background execution of the state machine),
  • false (if the end is reached),
  • an exception.
    The promise is implemented as a ManualResetValueTaskSourceCore (which is a re-usable and allocation-free way of producing and fulfilling ValueTask or ValueTask instances)
    and its surrounding interfaces on the state machine: IValueTaskSource and IValueTaskSource.
    See more details about those types at https://blogs.msdn.microsoft.com/dotnet/2018/11/07/understanding-the-whys-whats-and-whens-of-valuetask/

Compared to the state machine for a regular async method, the MoveNext() for an async-iterator method adds logic:

  • to support handling a yield return statement, which saves the current value and fulfills the promise with result true,
  • to support handling a yield break statement, which sets the dispose mode on and jumps to the enclosing finally or exit,
  • to dispatch execution to finally blocks (when disposing),
  • to exit the method, which disposes the CancellationTokenSource (if any) and fulfills the promise with result false,
  • to catch exceptions, which disposes the CancellationTokenSource (if any) and sets the exception into the promise.
    (The handling of an await is unchanged)

This is reflected in the implementation, which extends the lowering machinery for async methods to:

  1. handle yield return and yield break statements (see methods VisitYieldReturnStatement and VisitYieldBreakStatement to AsyncIteratorMethodToStateMachineRewriter),
  2. handle try statements (see methods VisitTryStatement and VisitExtractedFinallyBlock in AsyncIteratorMethodToStateMachineRewriter)
  3. produce additional state and logic for the promise itself (see AsyncIteratorRewriter, which produces various other members: MoveNextAsync, Current, DisposeAsync,
    and some members supporting the resettable ValueTask behavior, namely GetResult, SetStatus, OnCompleted).
csharp
ValueTask<bool> MoveNextAsync()
{
    if (state == StateMachineStates.FinishedStateMachine)
    {
        return default(ValueTask<bool>);
    }
    valueOrEndPromise.Reset();
    var inst = this;
    builder.Start(ref inst);
    var version = valueOrEndPromise.Version;
    if (valueOrEndPromise.GetStatus(version) == ValueTaskSourceStatus.Succeeded)
    {
        return new ValueTask<bool>(valueOrEndPromise.GetResult(version));
    }
    return new ValueTask<bool>(this, version); // note this leverages the state machine's implementation of IValueTaskSource<bool>
}
csharp
T Current => current;

The kick-off method and the initialization of the state machine for an async-iterator method follows those for regular iterator methods.
In particular, the synthesized GetAsyncEnumerator() method is like GetEnumerator() except that it sets the initial state to to StateMachineStates.NotStartedStateMachine (-1):

csharp
IAsyncEnumerator<T> GetAsyncEnumerator(CancellationToken token)
{
    {StateMachineType} result;
    if (initialThreadId == /*managedThreadId*/ && state == StateMachineStates.FinishedStateMachine)
    {
        state = InitialState; // -3
        builder = AsyncIteratorMethodBuilder.Create();
        disposeMode = false;
        result = this;
    }
    else
    {
        result = new {StateMachineType}(InitialState);
    }
    /* copy each parameter proxy, or in the case of the parameter marked with [EnumeratorCancellation] combine it with `GetAsyncEnumerator`'s `token` parameter */
}

For the parameter with [EnumeratorCancellation], GetAsyncEnumerator initializes it by combining the two available tokens:

csharp
if (this.parameterProxy.Equals(default))
{
    result.parameter = token;
}
else if (token.Equals(this.parameterProxy) || token.Equals(default))
{
    result.parameter = this.parameterProxy;
}
else
{
    result.combinedTokens = CancellationTokenSource.CreateLinkedTokenSource(this.parameterProxy, token);
    result.parameter = combinedTokens.Token;
}

For a discussion of the threadID check, see https://github.com/dotnet/corefx/issues/3481

Similarly, the kick-off method is much like those of regular iterator methods:

csharp
{
    {StateMachineType} result = new {StateMachineType}(StateMachineStates.FinishedStateMachine); // -2
    /* save parameters into parameter proxies */
    return result;
}

Disposal

Iterator and async-iterator methods need disposal because their execution steps are controlled by the caller, which could choose to dispose the enumerator before getting all of its elements.
For example, foreach (...) { if (...) break; }.
In contrast, async methods continue running autonomously until they are done. They are never left suspended in the middle of execution from the caller's perspective, so they don't need to be disposed.

In summary, disposal of an async-iterator works based on four design elements:

  • yield return (jumps to finally when resuming in dispose mode)
  • yield break (enters dispose mode and jumps to enclosing finally)
  • finally (after a finally we jump to the next enclosing one)
  • DisposeAsync (enters dispose mode and resumes execution)

The caller of an async-iterator method should only call DisposeAsync() when the method completed or was suspended by a yield return.
DisposeAsync sets a flag on the state machine ("dispose mode") and (if the method wasn't completed) resumes the execution from the current state.
The state machine can resume execution from a given state (even those located within a try).
When the execution is resumed in dispose mode, it jumps straight to the enclosing finally.
finally blocks may involve pauses and resumes, but only for await expressions. As a result of the restrictions imposed on yield return (described above), dispose mode never runs into a yield return.
Once a finally block completes, the execution in dispose mode jumps to the next enclosing finally, or the end of the method once we reach the top-level.

Reaching a yield break also sets the dispose mode flag and jumps to the enclosing finally (or end of the method).
By the time we return control to the caller (completing the promise as false by reaching the end of the method) all disposal was completed,
and the state machine is left in finished state. So DisposeAsync() has no work left to do.

Looking at disposal from the perspective of a given finally block, the code in that block can get executed:

  • by normal execution (ie. after the code in the try block),
  • by raising an exception inside the try block (which will execute the necessary finally blocks and terminate the method in Finished state),
  • by calling DisposeAsync() (which resumes execution in dispose mode and jumps to the enclosing finally),
  • following a yield break (which enters dispose mode and jumps to the enclosing finally),
  • in dispose mode, following a nested finally.

A yield return is lowered as:

csharp
_current = expression;
_state = <next_state>;
goto <exprReturnTruelabel>; // which does _valueOrEndPromise.SetResult(true); return;

// resuming from state=<next_state> will dispatch execution to this label
<next_state_label>: ;
this.state = cachedState = NotStartedStateMachine;
if (disposeMode) /* jump to enclosing finally or exit */

A yield break is lowered as:

csharp
disposeMode = true;
/* jump to enclosing finally or exit */
csharp
ValueTask IAsyncDisposable.DisposeAsync()
{
    if (state >= StateMachineStates.NotStartedStateMachine /* -1 */)
    {
        throw new NotSupportedException();
    }
    if (state == StateMachineStates.FinishedStateMachine /* -2 */)
    {
        return default;
    }
    disposeMode = true;
    _valueOrEndPromise.Reset();
    var inst = this;
    _builder.Start(ref inst);
    return new ValueTask(this, _valueOrEndPromise.Version);  // note this leverages the state machine's implementation of IValueTaskSource
}

Regular versus extracted finally

When the finally clause contains no await expressions, a try/finally is lowered as:

csharp
try
{
    ...
    finallyEntryLabel:
}
finally
{
    ...
}
if (disposeMode) /* jump to enclosing finally or exit */

When a finally contains await expressions, it is extracted before async rewriting (by AsyncExceptionHandlerRewriter). In those cases, we get:

csharp
try
{
    ...
    goto finallyEntryLabel;
}
catch (Exception e)
{
    ... save exception ...
}
finallyEntryLabel:
{
    ... original code from finally and additional handling for exception ...
}

In both cases, we will add a if (disposeMode) /* jump to enclosing finally or exit */ after the block for finally logic.

State values and transitions

The enumerable starts with state -2.
Calling GetAsyncEnumerator sets the state to -3, or returns a fresh enumerator (also with state -3).

From there, MoveNext will either:

  • reach the end of the method (-2, we're done and disposed)
  • reach a yield break (state unchanged, dispose mode = true)
  • reach a yield return (-N, decreasing from -4)
  • reach an await (N, increasing from 0)

From suspended state N or -N, MoveNext will resume execution (-1).
But if the suspension was a yield return (-N), you could also call DisposeAsync, which resumes execution (-1) in dispose mode.

When in dispose mode, MoveNext continues to suspend (N) and resume (-1) until the end of the method is reached (-2).

The result of invoking DisposeAsync from states -1 or N is unspecified. This compiler generates throw new NotSupportException() for those cases.

text
DisposeAsync                              await
 +------------------------+             +------------------------> N
 |                        |             |                          |
 v   GetAsyncEnumerator   |             |        resuming          |
-2 --------------------> -3 --------> -1 <-------------------------+    Dispose mode = false
 ^                                   |  |                          |
 |         done and disposed         |  |      yield return        |
 +-----------------------------------+  +-----------------------> -N
 |        or exception thrown        |                             |
 |                                   |                             |
 |                             yield |                             |
 |                             break |           DisposeAsync      |
 |                                   |  +--------------------------+
 |                                   |  |
 |                                   |  |
 |         done and disposed         v  v    suspension (await)
 +----------------------------------- -1 ------------------------> N
                                        ^                          |    Dispose mode = true
                                        |         resuming         |
                                        +--------------------------+

Features Deconstruction (`docs/features/deconstruction.md`)

Quickstart guide for deconstructions (C# 7.0)

  1. Install Visual Studio 2017

  2. Start a C# project

  3. Add a reference to the System.ValueTuple package from NuGet

  4. Use deconstructions:

c#
public class C
{
        public static void Main()
        {
                
              int code;
              string message;

              var pair = (42, "hello");
              (code, message) = pair; // deconstruct a tuple into existing variables
              Console.Write(message); // hello

              (code, message) = new Deconstructable(); // deconstruct any object with a proper Deconstruct method into existing variables
              Console.Write(message); // world
              
              (int code2, string message2) = pair; // deconstruct into new variables
              var (code3, message3) = new Deconstructable(); // deconstruct into new 'var' variables
        }
}

public class Deconstructable
{
        public void Deconstruct(out int x, out string y)
        {
                x = 43;
                y = "world";
        }
}

Design

This design doc will cover two kinds of deconstruction: deconstruction into existing variables (deconstruction-assignment) and deconstruction into new variables (deconstruction-declaration).

Here is an example of deconstruction-assignment:

c#
class C
{
    static void Main()
    {
        long x;
        string y;

        (x, y) = new C();
        System.Console.WriteLine(x + "" "" + y);
    }

    public void Deconstruct(out int a, out string b)
    {
        a = 1;
        b = ""hello"";
    }
}

Deconstruction-assignment (deconstruction into existing variables):

This doesn't introduce any changes to the language grammar. We have an assignment-expression (also simply called assignment in the C# grammar) where the unary-expression (the left-hand-side) is a tuple-expression containing values that can be assigned to.
In short, what this does in the general case is find a Deconstruct method on the expression on the right-hand-side of the assignment, invoke it with the appropriate number of out var parameters, converts those output values (if needed) and assign them to the variables on the left-hand-side. But in the special case where the expression on the right-hand-side is a tuple (tuple expression or tuple type), then the elements of the tuple are assigned to the variables on the left-hand-side without calling Deconstruct.

If the left-hand-side is nested the process will be repeated. For instance, in (x, (y, z)) = deconstructable;, deconstructable will be deconstructed into two parts and its second part will be further deconstructed.

In the case where the expression on the right is a tuple expression, it is first given a type. So in long x; string y; (x, y) = (1, null); the literals on the right-hand-side are typed as long and string before the deconstruction even starts, which means that no conversions will be needed during the deconstruction steps.

We noted already that tuples (which are syntactic sugar for the System.ValueTuple underlying type) don't need to invoke Deconstruct.
The .NET framework also includes a set of System.Tuple types. Those are not recognized as C# tuples, and so will rely on the Deconstruct pattern. Those Deconstruct methods will be provided as extension methods for System.Tuple for up to 3 nestings deep (that is 21 elements).

A deconstruction-assignment returns a tuple value (with elements using default names) which is shaped and typed like the left-hand-side and holds the (converted) parts resulting from deconstruction.

Evaluation order

The evaluation order can be summarized as: (1) all the side-effects on the left-hand-side, (2) all the Deconstruct invocations (if not tuple), (3) conversions (if needed), and (4) assignments.

In the general case, the lowering for deconstruction-assignment would translate: (expressionX, expressionY, expressionZ) = expressionRight into:

text
// do LHS side-effects
tempX = &evaluate expressionX
tempY = &evaluate expressionY
tempZ = &evaluate expressionZ

// do Deconstruct
evaluate right and evaluate Deconstruct in three parts (tempA, tempB and tempC)

// do conversions
tempConvA = convert tempA
tempConvB = convert tempB
tempConvC = convert tempC

// do assignments
tempX = tempConvA
tempY = tempConvB
tempZ = tempConvC

The evaluation order for nesting (x, (y, z)) is:

text
// do LHS side-effects
tempX = &evaluate expressionX
tempY = &evaluate expressionY
tempZ = &evaluate expressionZ

// do Deconstruct
evaluate right and evaluate Deconstruct into two parts (tempA and tempNested)
evaluate Deconstruct on tempNested intwo two parts (tempB and tempC)

// do conversions
tempConvA = convert tempA
tempConvB = convert tempB
tempConvC = convert tempC

// do assignments
tempX = tempConvA
tempY = tempConvB
tempZ = tempConvC

The evaluation order for the simplest cases (locals, fields, array indexers, or anything returning ref) without needing conversion:

text
evaluate side-effect on the left-hand-side variables
evaluate Deconstruct passing the references directly in

Resolution of the Deconstruct method

The resolution is equivalent to typing rhs.Deconstruct(out var x1, out var x2, ...); with the appropriate number of parameters to deconstruct into.
It is based on normal overload resolution.
This implies that rhs cannot be dynamic and that none of the parameters of the Deconstruct method can be type arguments. A Deconstruct<T>(out T x1, out T x2) method will not be found.
Also, the Deconstruct method must be an instance method or an extension (but not a static method). It also must return void.

Deconstruction-declaration (deconstruction into new variables):

The deconstruction-declaration is also represented with an assignment, but the left-hand-side is either a declaration-expression or a tuple expression containing declaration-expressions.
Deconstruction-declarations can be thought of as two steps: (1) declaring new locals, and (2) applying a deconstruction-assignment into those locals.
The declaration of new locals by the left-hand-side of the assignment can take multiple forms. The simplest case is (int x, string y), that is a tuple expression containing declaration expressions. Variants include nested declarations like (int x, (string y, long z)) (which declares 3 locals) and implicitly typed declarations like (var x, var y). The latter can also be written using the shorthand var (x, y), which is a declaration expression with parenthesized designations.
var is the only case where such shorthand is allowed (so int (x, y) is not legal).

As in the case of deconstruction-declarations, tuple expressions on the right-hand-side have their type inferred from the left-hand-side. With deconstruction-declaration, this is also the case, except that any type is var in the left-hand-side, then the natural type of the element in the right-hand-side is used.
For example, in (string x, byte y, var z) = (null, 1, 2);, null has type string, the literal 1 has type byte (inferred from y) and the literal 2 has type int (its natural type).

In C#7.0, deconstruction-declarations are only allowed as top-level statements. They do not allow mixing of declaration expressions and assignable expressions (such as (existing, var declared) = (1, 2)).

Grammar changes

antlr
expression
	: ... // existing
	| declaration_expression // new (only allowed in C#7.0 in certain contexts, such as out var, deconstruction and pattern declarations)
	;

declaration_expression // new
	: type variable_designation
	;

variable_designation // new
	: single_variable_designation
	| parenthesized_variable_designation
	| discard_designation
	;

single_variable_designation // new
	: identifier
	;

parenthesized_variable_designation // new
	: '(' variable_designation (',' variable_designation)+ ')'
	;

discard_designation // new
	: '_'
	;

foreach_variable_statement // new
    : 'foreach' '(' declaration_expression 'in' expression ')' embedded_statement
    ;

References

C# Design Notes for Oct 25-26, 2016

C# Design Notes for Sep 6, 2016

C# Design Notes for July 13, 2016

C# Design Notes for May 3-4, 2016

C# Design Notes for Apr 12-22, 2016

Design for declaration expressions

The What's new in C# 7.0 post has a section on deconstructions.

Possible future expansions

See C# Lang repo for more up-to-date proposals and discussions.


Features Discards (`docs/features/discards.md`)

Discards

Discards are variables which you can assign to, but cannot read from. They don't have names. Instead, they are represented by an '_' (underscore).
In C#7.0, they can appear in the following contexts:

  • out variable declarations, such as bool found = TryGetValue(out var _) or bool found = TryGetValue(out _)
  • deconstruction assignments, such as (x, _) = deconstructable;
  • deconstruction declarations, such as (var x, var _) = deconstructable;
  • is patterns, such as x is int _
  • switch/case patterns, such as case int _:

The principal representation of discards is an _ (underscore) designation in a declaration expression. For example, int _ in an out variable declaration or var (_, _, x) in a deconstruction declaration.

The second representation of discards is using the expression _ as a short-hand for var _, when no variable named _ is in scope. It is allowed in out vars, deconstruction assignments and declarations, and plain assignments (_ = IgnoredReturn();). It is, however, not allowed in C#7.0 patterns.
When a variable named _ does exist in scope, then the expression _ is simply a reference to that variable, as it did in earlier versions of C#.

###Grammar changes

antlr
declaration_expression
	: type variable_designation
	;

variable_designation
	: single_variable_designation
	| parenthesized_variable_designation
	| discard_designation // new
	;

discard_designation // new
	: '_'
	;

References
C# Language Design Notes for Oct 25 and 26, 2016
C# Design Notes for Oct 18, 2016
Design proposal for discards
Note that those notes used to refer to discards as "wildcards".


Features Dynamic Analysis.Work (`docs/features/dynamic-analysis.work.md`)

Dynamic Analysis

This feature enables instrumentation of binaries to collect information for dynamic analysis.


TODO:

  • Generate code to compute MVIDs less often than once per invoked method.
  • Synthesize helper types to contain fields for analysis payloads so that methods of generic types can be instrumented.
  • Accurately identify which methods are tests.
  • Integrate dynamic analysis instrumentation with lowering.
  • Verify that instrumentation covers constructor initializers and field initializers.

Features Enhanced Using (`docs/features/enhanced-using.md`)

Enhanced Using

Enhanced using consists of several related features that aim to make the disposable coding pattern easier to participate in for implementers, and easier to consume for end-users.

See: the corresponding proposal in CSharpLang.

Using Declarations

Using declarations allow a user to specify a using keyword as part of a local declaration statement:

csharp
using var x = ...
// other statements

This is equivalent to declaring the local inside of a using statement at the same location:

csharp
using (var x = ...) 
{
    // other statements
}

It is not valid to declare a using declaration without an initializer expression:

csharp
using IDisposable x; //error CS0210: You must provide an initializer in a fixed or using statement declaration

The initializer expression must result in a type that is considered to be Disposable. That is, the expression must also be valid when used directly inside a using statement:

csharp
using var x = <expression> 

using (<expression>) { } // expression must also be valid in a standard using statement

Lifetime

The lifetime of the local extends to the scope in which it is declared; immediately prior to the variable going out of scope, it will be disposed.

csharp
if (...)
{
    using var x = ...;
    
    // other statements

    // Dispose x
}

Using declarations in the same scope are disposed in the reverse order to which they are declared

csharp
{
    using var x = ...;
    using var y = ...,  z = ...;

    // Dispose z
    // Dispose y
    // Dispose x
}

As with a local declared as part of a using statement, a using local is readonly and may not be re-assigned after declaration.

csharp
using var x = ...;
x = ...; // error CS1656: Cannot assign to 'x' because it is a 'using variable'

A using local may be used in the right hand side of an assignment or declaration. This means it is possible to capture a reference that will exist for longer than the lifetime of the using local. The reference will still be disposed when the using local is going out of scope. Interacting with the reference after disposal is undefined, but in most cases it is expected that it would result in an ObjectDisposedException being thrown.

csharp
IDisposable y = null;
if (...)
{
    using var x = ...;
    y = x;
    // Dispose x
}
y.Dispose(); // undefined. ObjectDisposedException in most cases

It is possible to use an existing reference as the initializer of a using local declaration. As above, the reference will be disposed when the declared local is going out of scope, but the existing reference will still be available for use via its previous declaration.

csharp
IDisposable y = ...;
if (...)
{
    using var x = y; 
    // Dispose x, which points to the same object as y
}
y.Dispose(); // undefined. ObjectDisposedException in most cases

Asynchronous disposal

When inside a method marked async, a user may optionally specify an additional await keyword prior to the using keyword, to indicate asynchronous disposal:

csharp
await using var x = ...

Which is equivalent to

csharp
await using (var x = ...) { }

The initializer expression must result in a type that is considered to be asynchronously disposable. That is, the expression must also be valid when used directly in an await using statement:

csharp
await using var x = <expression> 

await using (<expression>) { } // expression must also be valid in a standard await using statement

Attempting to specify the await keyword on a using declaration in a method not marked with the async keyword results in a compiler error:

csharp
public void M()
{
    await using var x = ...; //error CS4033: 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'.
}

Control flow

In general, control flow is unaffected by the presence of a using declaration. However, there are some restrictions around the use of the goto statement when the statement and its target label lie either side of a using declaration.

It is forbidden to jump 'forward' to a location after a using declaration, when the goto or the target label are in the same or lower scope as the declaration.

csharp
goto label1; // error CS8648: A goto cannot jump to a location after a using declaration.
    using var x = ...;
label1:
    return;
csharp
goto label1; // ok. using declaration is in a lower scope than goto and label
    {
        using var x = ...;
    }
label1:
    return;
csharp
{
        {
            goto label1; // error CS8648: A goto cannot jump to a location after a using declaration.          
        }
        using var x = ...;
    }
label1:
    return;

It is forbidden to jump 'backwards' to a location before a using declaration, when the label is in the same scope as the using declaration.

csharp
label1:
        using var x = ...; //error CS8649: A goto cannot jump to a location before a using declaration within the same block.
        goto label1;

Jumping 'backwards' to a label at a higher scope is specifically permitted.

csharp
label1:
    {
        using var x = ...;
        goto label1; // allowed. label1 is in a higher scope than the using declaration
    }

When jumping to a higher scope, any declared using variables will be disposed at the site of the goto. Variables not yet declared will not be disposed.

csharp
label1:
    {
        using var x = ...;
        goto label1; // dispose x
        using var y = ...; 
    }

It is always permissible to jump to the same scope, when the goto and target label do not lie either side of a using declaration.

csharp
goto label1; // ok, not jumping over a using declaration
label1:
    using var x = ...;
label2:
    goto label2; // ok, not jumping over a using declaration

Other restrictions on use

A using declaration may not appear directly inside of a case label. It may instead be used within a block inside of a case label:

csharp
switch (...)
{
    case ...: 
        using var x = ...; // error CS8647: A using variable cannot be used directly within a switch section (consider using braces). 
        break;
    
    case ...:
        {
            using var y = ...; // ok

            // Dispose y
        }
        break;
}

A using declaration may not appear directly as part of an out variable declaration. It is easy to emulate this behavior however, by adding a using declaration immediately after the out variable:

csharp
if(TryGetDisposable(out var x))
{
    using var y = x;

    // Dispose y, and thus x
}

Pattern-based asynchronous disposal

With pattern-based asynchronous disposal a type can be used in an await using statement without needing to explicitly implement IAsyncDisposable if it meets certain structural requirements. Specifically:

text
"A reachable, non-generic Task-like returning instance method called DisposeAsync, that can be called with zero explicit arguments"

Where reachable means legal to call from the site of the await using(...) under normal accessibility rules.

csharp
public class C 
{
    public static async Task M()
    {     
        await using (AsyncDisposer a = new AsyncDisposer())
        { 
        }
    }
}

public class AsyncDisposer
{
    public async ValueTask DisposeAsync() => Console.WriteLine("DisposeAsync");
}

In the situation where a type can both be implicitly converted to IAsyncDisposable and also fits the asynchronous disposal pattern, IAsyncDisposable is chosen.

Optional arguments and params

Pattern-based asynchronous disposal methods may contain optional or params parameters and still meet the requirements of the pattern. In general if you could write c.DisposeAsync() at the site of the await using syntax and have it be a valid call under normal language rules, then the type is considered to be asynchronously disposable.

csharp
public class AsyncDisposer
{
    public async ValueTask DisposeAsync(int x = 0, params object[] args) => Console.WriteLine("DisposeAsync"); // valid pattern candidate
}

Extension methods

Extension methods may not be used to implement asynchronous disposal. The method must be a reachable instance method in order to be considered as a valid candidate for the pattern.

Nullable value type behavior

Note: This is currently not working as spec'd and is tracked by #34701

For nullable value types in a using statement, the behavior today is to call Dispose on the underlying type if and only if the type has a value (i.e. if(t.HasValue){ t.GetValueOrDefault().Dispose() } ). This allows lifted types to be used as if they were the underlying type, and dispose is only called in the case they are not null. This behavior is extended to pattern-based DisposeAsync methods in await using statements.

csharp
public class C 
{
    public static async Task M()
    {
        StructDisposer? a = null;
        await using (a) { } // DisposeAsync is not invoked
        
        StructDisposer? b = new StructDisposer();
        await using (b) { } // DisposeAsync is invoked
    }
}

public struct StructDisposer
{
    public async ValueTask DisposeAsync() => Console.WriteLine("DisposeAsync");
}

Pattern-based disposal for ref structs

Today ref structs can not participate in the IDisposable pattern as they can not implement an interface. This feature allows a ref struct to be considered disposable if it meets certain structural requirements. Specifically:

text
"A reachable, void returning instance method called Dispose, that can be called with zero explicit arguments"

Where reachable means legal to call from the site of the using(...) under normal accessibility rules.

csharp
using System;
public class C 
{
    public void M()
    {
        using var x = new Disposer();
    }
}

ref struct Disposer
{
    internal void Dispose() => Console.WriteLine("Disposed");
}

Note: ref structs can not be used in an async method and therefore can not participate in asynchronous disposal via await using

Optional arguments and params

Pattern-based disposal methods may contain optional or params parameters and still meet the requirements of the pattern. In general if you could write s.Dispose() at the site of the using syntax and have it be a valid call under normal language rules, then the ref struct is considered to be disposable.

csharp
public ref struct Disposer
{
    public void Dispose(int x = 0, params object[] args) => Console.WriteLine("Disposed"); // valid pattern candidate
}

Extension methods

Extension methods may not be used to implement disposal. The method must be a reachable instance method in order to be considered as a valid candidate for the pattern.


Features Field Nullability (`docs/features/field-nullability.md`)

`field` keyword nullable analysis implementation

See also specification.

Symbol API behavior

In order to decide the NullableAnnotation of a SynthesizedBackingFieldSymbol we need to:

  1. Decide if the getter associated with this field requires a null-resilience analysis.
  2. If it doesn't require such an analysis, then the nullable annotation of the property is used.
  3. If it does, then a binding+nullable analysis of the getter must be performed to decide the nullability of the backing field.

There are some significant problems with directly exposing the nullable annotation on this field symbol.

  1. Cost. It's not clear if it's OK for reading the NullableAnnotation off a field to cause us to bind and nullable analyze something. If some tooling is traversing member symbols for indexing or some such, it may be a problem if that causes unexpected method binding to occur where it didn't before.
  2. Stack cycles. In the process of binding+flow analyzing, we may want to access the nullable annotation of the field. If we are already in the process of determining that field's nullable annotation, then we would need to "short-circuit" and return some "placeholder" value. We would need to take care to isolate the "internal" implementation from any public implementation, in order to ensure that a consistent answer is given to users of the public API.
    ...

It's tempting to avoid exposing the inferred nullable annotation in the public symbol model. This might be problematic for automated tooling which is trying to reason about nullable initialization. For example, if a diagnostic suppressor wants to suppress CS8618 nullable initialization warnings under certain conditions. How should it decide whether/why a property like string Prop => field ??= GetValue(); requires initialization in constructors? It's unclear to me whether this matters.

In the interests of simplicity (first make it correct, then make it fast), I'd like to move forward by not exposing the inferred annotation in the symbol model. Rather, we introduce a new internal API SynthesizedBackingFieldSymbol.GetInferredNullableAnnotation(), which NullableWalker will use to decide initial nullable state, report warnings, and so on. The implementation simply does an on-demand binding of the get accessor, then nullable analyzes and decides the nullable annotation. Care needs to be taken to avoid using this API in a re-entrant manner--so, the NullableWalker passes which are used to infer the nullability annotation must avoid calling it.

This means that the ordinary FieldSymbol.TypeWithAnnotations API would not expose the inferred nullability, and neither would IFieldSymbol.Type or IFieldSymbol.NullableAnnotation. Instead the field's nullability would always match the property's. This is something I would actually like to fix, maybe before merging to main. It feels like Quick Info, etc., should expose the inferred nullability through the ordinary APIs.

Once we get a handle on the behaviors of the getter null resilience, we can start talking about how to reduce cost associated with it. For example, we could try to reduce redundant binding, by forcing nullable annotations to be inferred prior to analyzing certain methods. Before processing a constructor in MethodCompiler, for example, maybe we would identify the getters that need to be compiled for purposes of inference, and force those to compile first.


Features File Based Programs Vscode (`docs/features/file-based-programs-vscode.md`)

File-based apps VS Code support

File-based app and file-based program are synonymous.

See also:

Feature overview

A file-based app embeds a subset of MSBuild project capabilities into C# code, allowing single files to stand alone as ordinary projects.

The following is a file-based app:

cs
Console.WriteLine("Hello World!");

So is the following:

cs
#!/usr/bin/env dotnet run
#:sdk Microsoft.NET.Sdk
#:package [email protected]
#:property LangVersion=preview

using Newtonsoft.Json;

Main();

void Main()
{
    if (args is not [_, var jsonPath, ..])
    {
        Console.Error.WriteLine("Usage: app <json-file>");
        return;
    }

    var json = File.ReadAllText(jsonPath);
    var data = JsonConvert.DeserializeObject<Data>(json);
    // ...
}

record Data(string field1, int field2);

The dotnet command line interprets the #: directives in source files, produces a C# project XML document in memory, and passes it off to MSBuild. The in-memory project is sometimes called a "virtual project".

Rich miscellaneous files

There is long-standing tracking to enhance the experience of working with miscellaneous files ("loose files" not associated with any project), including:

As part of the file-based app work, we can enable the following in such files without substantial issues:

  • Syntax diagnostics.
  • IntelliSense for the "default" set of references, for example 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.

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 #:included by the entry point of the same.
  • Miscellaneous File With Standard References and Semantic Errors. The file is a valid entry point for 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.
    • 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 Miscellaneous File With Standard References
    • Yes → Continue to next check
  5. Does the file have #! directives?

    • Yes → Classify as File-Based App. Restore if needed and show semantic errors.
    • No → Continue to next check
  6. Does the file have #: directives?

    • No → Go to (8)
    • Yes → Continue to next check
  7. Does the file have top-level statements?

    • Yes → Classify as File-Based App. Restore if needed and show semantic errors.
    • No → Classify as Miscellaneous File With Standard References
  8. 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):

  1. Does the file have top-level statements?

    • No → Classify as Miscellaneous File With Standard References
    • Yes → Continue to next check
  2. 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

We added an opt-out flag with option name dotnet.projects.enableFileBasedPrograms. If issues arise with the file-based app experience, then VS Code users should set the corresponding setting "dotnet.projects.enableFileBasedPrograms": false to revert back to the old miscellaneous files experience.

We also have a second, finer-grained opt-out flag dotnet.projects.enableFileBasedProgramsWhenAmbiguous. This flag is conditional on the previous flag (i.e. it is ignored when enableFileBasedPrograms is false). This is used to allow opting out only in cases where it is unclear from the single file itself, whether it should be treated as a file-based app. Presence of #: or #! directives in a .cs file strongly indicates that the file is a file-based app, and editor functionality will continue to light up for such files, even when enableFileBasedProgramsWhenAmbiguous is false.

[!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 app 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, 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 <Compile Include="Util.cs" />

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. For example, 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 <Compile Include="App1.cs" /> <-- 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 two 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 apps and miscellaneous files.

This project system effectively performs the classification process described in 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.

The project system uses the file-based app 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. This work was tracked by PR 82863: file-based apps automatic discovery.
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 that 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. Related work is tracked by:

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 put #!-at-start as a standard for entry points of file-based apps. We report a warning in files which contain both #:include and top-level statements, but do not have #! at the top. We did this in PR 80575: File-based programs live directive diagnostics.

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. 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 stop using this designation for files which exist on disk. Instead, we may identify files that (1) contain top-level statements, (2) are not part of an ordinary project, and (3) have no .csproj in-cone. We'd classify these files as 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 miscellaneous file, things may be broken" popup. Related tracking includes issue 81252: Do not restore loose files which lack file-based app directives and PR 84385: Extract file-based app entry point heuristic.

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. Related work is tracked by:

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. This is tracked by issue 78878: Determine if a source file contains top-level statements without doing a full additional parse.


Features Generators (`docs/features/generators.md`)

Source Generators

Deprecation notice

This proposal has been deprecated in favor of the newer Source Generators proposal.


Features Incremental Generators.Cookbook (`docs/features/incremental-generators.cookbook.md`)

Incremental Generators Cookbook

Summary

This document aims to be a guide to help the creation of source generators by providing a series of guidelines for common patterns.
It also aims to set out what types of generators are possible under the current design, and what is expected to be explicitly out
of scope in the final design of the shipping feature.

This document expands on the details in the full design document, please ensure you have read that first.

Table of contents

Proposal

As a reminder, the high level design goals of source generators are:

  • Generators produce one or more strings that represent C# source code to be added to the compilation.
  • Explicitly additive only. Generators can add new source code to a compilation but may not modify existing user code.
  • May access additional files, that is, non-C# source texts.
  • Run un-ordered, each generator will see the same input compilation, with no access to files created by other source generators.
  • A user specifies the generators to run via list of assemblies, much like analyzers.
  • Generators create a pipeline, starting from base input sources and mapping them to the output they wish to produce. The more exposed,
    properly equatable states exist, the earlier the compiler will be able to cut off changes and reuse the same output.

Out of scope designs

We will briefly look at the non-solvable problems as examples of the kind of problems source generators are not designed to solve:

Language features

Source generators are not designed to replace new language features: for instance one could imagine records being implemented as a source generator
that converts the specified syntax to a compilable C# representation.

We explicitly consider this to be an anti-pattern; the language will continue to evolve and add new features, and we don't expect source generators to be
a way to enable this. Doing so would create new 'dialects' of C# that are incompatible with the compiler without generators. Further, because generators,
by design, cannot interact with each other, language features implemented in this way would quickly become incompatible with other additions to the language.

Code rewriting

There are many post-processing tasks that users perform on their assemblies today, which here we define broadly as 'code rewriting'. These include, but
are not limited to:

  • Optimization
  • Logging injection
  • IL Weaving
  • Call site re-writing

While these techniques have many valuable use cases, they do not fit into the idea of source generation. They are, by definition, code altering operations
which are explicitly ruled out by the source generators proposal.

There are already well supported tools and techniques for achieving these kinds of operations, and the source generators proposal is not aimed at replacing them.
We are exploring approaches for call site rewriting (see interceptors.md), but those features are experimental and may change significantly
or even be removed.

Conventions

Pipeline model design

As a general guideline, source generator pipelines need to pass along models that are value equatable. This is critical to the incrementality of an
IIncrementalGenerator; as soon as a pipeline step returns the same information that it returned in the previous run, the generator driver can stop running
the generator and reuse the same cached data that the generator produced in the previous run. Most times a generator is triggered (particularly generators that
need to look at type or method definitions, using ForAttributeWithMetadataName) the edit that triggered the generator will not actually have affected the
things that your generator is looking at. However, because semantics can change on basically any edit, the generator driver must rerun your generator again
to ensure that this is the case. If your generator then produces a model with the same values as it did previously, this short-circuits the pipeline and allows
us to avoid a lot of work. Here are some general guidelines around designing your models to ensure that you maintain this equality:

  • Use records, rather than classes, so that value equality is generated for you.
  • Symbols (ISymbol and anything that inherits from that interface) are never equatable, and including them in your model can potentially root old compilations
    and force Roslyn to hold onto lots of memory that it could otherwise free. Never put these in your model types. Instead, extract the information you need from
    the symbols you inspect to an equatable representation: strings often work quite well here.
  • SyntaxNodes are also usually not equatable between runs. They're not as strongly discouraged from the initial stages of a pipeline as symbols, and an example
    later down shows a case where you will need to include a SyntaxNode in model. They also don't potentially root as much
    memory as symbols will. However, any edit in a file will ensure that all SyntaxNodes from that file are no longer equatable, so they should be removed from
    your models as soon as possible.
  • The previous bullet applies to Locations as well.
  • Be careful of collection types in your models. Most built-in collection types in .NET do not do value equality by default. Arrays, ImmutableArray<T> and
    List<T>, for example, use reference equality, not value equality. We suggest that most generator authors use a wrapper type around arrays to augment them
    with value-based equality.

Use `ForAttributeWithMetadataName`

We highly recommend that all generator authors that need to inspect syntax do so by using a marker attribute to indicate the types or members that need to be
inspected. This has multiple benefits, both for you as an author, and also for your users:

  • As an author, you can use SyntaxProvider.ForAttributeWithMetadataName. This utility method is at least 99x more efficient than SyntaxProvider.CreateSyntaxProvider,
    and in many cases even more efficient. This will help you avoid causing performance issues for your users in editors.
  • Your users can clearly indicate that they intend to use your source generator. This intention is extremely helpful for designing a good user experience; it
    means that you can author Roslyn analyzers to help your users when they intended to use your generator but violated your rules in some fashion. For example, if
    you are generating some method body, and your generator requires that the user return a specific type, the presence of a GenerateMe attribute means you can
    write an analyzer to tell the user if their method declaration returns something that it shouldn't.

Use an indented text writer, not `SyntaxNode`s, for generation

We do not recommend generating SyntaxNodes when generating syntax for AddSource. Doing so can be complex, and it can be difficult to format it well; calling
NormalizeWhitespace is often quite expensive, and the API is not really designed for this use-case. Additionally, to ensure immutability guarantees, AddSource does
not accept SyntaxNodes. It instead requires getting the string representation and putting that into a SourceText. Instead of SyntaxNode, we recommend using a
wrapper around StringBuilder that will keep track of indent level and prepend the right amount of indentation when AppendLine is called. See
this conversation on the performance of NormalizeWhitespace for more examples, performance
measurements, and discussion on why we don't believe that SyntaxNodes are a good abstraction for this use case.

Put `Microsoft.CodeAnalysis.EmbeddedAttribute` on generated marker types

Users might depend on your generator in multiple projects in the same solution, and these projects will often have InternalsVisibleTo applied. This means that your
internal marker attributes may be defined in multiple projects, and the compiler will warn about this. While this doesn't block compilation, it can be irritating to
users. To avoid this, mark such attributes with Microsoft.CodeAnalysis.EmbeddedAttribute; when the compiler sees this attribute on a type from separate assembly or
project, it will not include that type in lookup results. To ensure that Microsoft.CodeAnalysis.EmbeddedAttribute is available in the compilation, call the
AddEmbeddedAttributeDefinition helper method in your RegisterPostInitializationOutput callback.

Another option is to provide an assembly in your nuget package that defines your marker attributes, but this can be more difficult to author. We recommend the
EmbeddedAttribute approach, unless you need to support versions of Roslyn lower than 4.14.

Do not scan for types that indirectly implement interfaces, indirectly inherit from types, or are indirectly marked by an attribute from an interface or base type

Using an interface/base type marker can be a very tempting and natural fit for generators. However, scanning for these types of markers is very expensive, and cannot
be done incrementally. Doing so can have an outsized impact on IDE and command-line performance, even for fairly small consuming users. These scenarios are:

  • A user implements an interface on BaseModelType, and then the generator looks all derived types from BaseModelType. Because the generator cannot know ahead of time
    what BaseModelType actually is, it means that the generator has to fetch AllInterfaces on every single type in the compilation so it can scan for the marker
    interface. This will end up occurring either on every keystroke or every file save, depending on what mode the user is running generators in; either one is disastrous
    for IDE performance, even when trying to optimize by scoping down the scanning to only types with a base list.
  • A user inherits from a generator-defined BaseSerializerType, and the generator looks for anything that inherits from that type, either directly or indirectly. Similar
    to the above scenario, the generator will need to scan all types with a base type in the entire compilation for the inherited BaseSerializerType, which will heavily
    impact IDE performance.
  • A generator looks among all base types/implemented interfaces for a type that is attributed with a generator's marker attribute. This is effectively either scenario 1
    or 2, just with a different search criteria.
  • A generator leaves its marker attribute unsealed, and expects users to be able to derive their own attributes from that marker, as a source of parameter customization.
    This has a couple of problems: first, every attributed type needs to be checked to see if the attribute inherits from the marker attribute. While not as performance
    impacting as the first three scenarios, this isn't great for performance. Second, and more importantly, there is no good way to retrieve any customizations from the
    inherited attribute. These attributes are not instantiated by the source generator, so any parameters passed to the base() constructor call or values that are assigned
    to any properties of the base attribute are not visible to the generator. Prefer using FAWMN-driven development here, and using an analyzer to inform the user if they
    need to inherit from some base class for your generator to work correctly.

Designs

This section is broken down by user scenarios, with general solutions listed first, and more specific examples later on.

Generated class

User scenario: As a generator author I want to be able to add a type to the compilation, that can be referenced by the user's code. Common use cases include
creating an attribute that will be used to drive other source generator steps.

Solution: Have the user write the code as if the type was already present. Generate the missing type based on information available in the compilation using
the RegisterPostInitializationOutput step.

Example:

Given the following user code:

csharp
public partial class UserClass
{
    [GeneratedNamespace.GeneratedAttribute]
    public partial void UserMethod();
}

Create a generator that will create the missing type when run:

csharp
[Generator]
public class CustomGenerator : IIncrementalGenerator
{
    public void Initialize(IncrementalGeneratorInitializationContext context)
    {
        context.RegisterPostInitializationOutput(static postInitializationContext => {
            postInitializationContext.AddEmbeddedAttributeDefinition();
            postInitializationContext.AddSource("myGeneratedFile.cs", SourceText.From("""
                using System;
                using Microsoft.CodeAnalysis;

                namespace GeneratedNamespace
                {
                    [Embedded]
                    internal sealed class GeneratedAttribute : Attribute
                    {
                    }
                }
                """, Encoding.UTF8));
        });
    }
}

Alternative Solution: If you are also providing a library to your users, in addition to a source generator, simply have that library include the
attribute definition.

Additional file transformation

User scenario: As a generator author I want to be able to transform an external non-C# file into an equivalent C# representation.

Solution: Use the AdditionalTextsProvider to filter for and retrieve your files. Transform them into the code you care about, then
register that code with the solution.

Example:

csharp
[Generator]
public class FileTransformGenerator : IIncrementalGenerator
{
    public void Initialize(IncrementalGeneratorInitializationContext context)
    {
        var pipeline = context.AdditionalTextsProvider
            .Where(static (text) => text.Path.EndsWith(".xml"))
            .Select(static (text, cancellationToken) =>
            {
                var name = Path.GetFileName(text.Path);
                var code = MyXmlToCSharpCompiler.Compile(text.GetText(cancellationToken));
                return (name, code);
            });

        context.RegisterSourceOutput(pipeline,
            static (context, pair) => 
                // Note: this AddSource is simplified. You will likely want to include the path in the name of the file to avoid
                // issues with duplicate file names in different paths in the same project.
                context.AddSource($"{pair.name}generated.cs", SourceText.From(pair.code, Encoding.UTF8)));
    }
}

Items need to be included in your csproj files by using the AdditionalFiles ItemGroup:

xml
<ItemGroup>
    <AdditionalFiles Include="file1.xml" />
    <AdditionalFiles Include="file2.xml" />
<ItemGroup>

Pre-compilation source generation

Note: this is an experimental API and requires suppressing diagnostic RSEXPERIMENTAL007 to use.

User scenario: As a generator author I want to produce source from non-compilation inputs (e.g. additional files, parse options) and have that source be visible to compilation-dependent code in the same generator (or in other generators), so that downstream code can call compilation.GetTypeByMetadataName(...) and find the types I just emitted.

Solution: Use RegisterPreCompilationSourceOutput. It runs in a new phase that executes before the standard RegisterSourceOutput phase, and its emitted source is added to the compilation that the standard phase observes. Because the phase runs before the compilation is fully built, the callback can only consume non-compilation providers (additional files, parse options, analyzer config options); attempting to reach CompilationProvider or SyntaxProvider from a pre-compilation pipeline throws at runtime.

Example:

csharp
[Generator]
public class ProtoGenerator : IIncrementalGenerator
{
    public void Initialize(IncrementalGeneratorInitializationContext context)
    {
        // Pre-compilation phase: read .proto files and emit C# types.
        // The emitted source becomes part of the compilation that the standard
        // phase below sees, so compilation.GetTypeByMetadataName("MyProto.Message")
        // will find the generated type.
        var protoFiles = context.AdditionalTextsProvider
            .Where(static (f) => f.Path.EndsWith(".proto"));

        context.RegisterPreCompilationSourceOutput(protoFiles, static (ctx, file) =>
        {
            var types = ParseProtoFile(file.GetText()!.ToString());
            ctx.AddSource($"{Path.GetFileName(file.Path)}.g.cs", GenerateTypes(types));
        });

        // Standard phase: the compilation now contains the generated proto types,
        // so we can look them up symbolically and emit code that depends on them.
        context.RegisterSourceOutput(context.CompilationProvider, static (ctx, compilation) =>
        {
            var messageType = compilation.GetTypeByMetadataName("MyProto.Message");
            if (messageType is not null)
            {
                ctx.AddSource("Serializers.g.cs", GenerateSerializer(messageType));
            }
        });
    }
}

When to use this over RegisterPostInitializationOutput: post-init runs even earlier, but takes no inputs at all; it is for emitting fixed source like marker attributes. If your generator needs to react to additional files, parse options, or analyzer config and have the result visible to downstream compilation-dependent code, pre-compilation is the right fit.

When to use this over RegisterSourceOutput: standard RegisterSourceOutput always runs after the compilation is built and cannot contribute source back into the compilation seen by other generators. Use pre-compilation when you specifically need that cross-generator visibility, e.g. one generator emits domain types from additional files and another generator (or a later pipeline in the same generator) needs to consume those types via CompilationProvider.

Diagnostics: PreCompilationSourceProductionContext intentionally does not include ReportDiagnostic. Pre-compilation is an early phase focused purely on producing source; diagnostic reporting should be done in a separate analyzer (see Issue Diagnostics below).

Augment user code

User scenario: As a generator author I want to be able to inspect and augment a user's code with new functionality.

Solution: Require the user to make the class you want to augment be a partial class, and mark it with a unique attribute.
Provide that attribute in a RegisterPostInitializationOutput step. Register for callbacks on that attribute with
ForAttributeWithMetadataName to collect the information needed to generate code, and use tuples (or create an equatable model)
to pass along that information. That information should be extracted from syntax and symbols; do not put syntax or symbols into
your models
.

Example:

csharp
public partial class UserClass
{
    [GeneratedNamespace.Generated]
    public partial void UserMethod();
}
text
/* Detailed source-code truncated for AI context efficiency. */

Issue Diagnostics

User Scenario: As a generator author I want to be able to add diagnostics to the user's compilation.

Solution: We do not recommend issuing diagnostics within generators. It is possible, but doing so without breaking incrementality is advanced topic
beyond the scope of this cookbook. Instead, we suggest writing a separate analyzer for reporting diagnostics.

INotifyPropertyChanged

User scenario: As a generator author I want to be able to implement the INotifyPropertyChanged pattern automatically for a user.

Solution: The design tenant 'Explicitly additive only' seems to be at direct odds with the ability to implement this, and appears to call for user code modification.
However we can instead take advantage of explicit fields and instead of editing the users properties, directly provide them for listed fields.

Example:

Given a user class such as:

csharp
using AutoNotify;

public partial class UserClass
{
    [AutoNotify]
    private bool _boolProp;

    [AutoNotify(PropertyName = "Count")]
    private int _intProp;
}

A generator could produce the following:

csharp
using System;
using System.ComponentModel;

namespace AutoNotify
{
    [AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)]
    sealed class AutoNotifyAttribute : Attribute
    {
        public AutoNotifyAttribute()
        {
        }
        public string PropertyName { get; set; }
    }
}


public partial class UserClass : INotifyPropertyChanged
{
    public bool BoolProp
    {
        get => _boolProp;
        set
        {
            _boolProp = value;
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("UserBool"));
        }
    }

    public int Count
    {
        get => _intProp;
        set
        {
            _intProp = value;
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Count"));
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;
}

Package a generator as a NuGet package

User scenario: As a generator author I want to package my generator as a NuGet package for consumption.

Solution: Generators can be packaged using the same method as an Analyzer would.
Ensure the generator is placed in the analyzers\dotnet\cs folder of the package for it to be automatically added to the users project on install.

For example, to turn your generator project into a NuGet package at build, add the following to your project file:

xml
<PropertyGroup>
    <GeneratePackageOnBuild>true</GeneratePackageOnBuild> 
    <IncludeBuildOutput>false</IncludeBuildOutput> 
  </PropertyGroup>

  <ItemGroup>
    
    <None Include="$(OutputPath)\$(AssemblyName).dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
  </ItemGroup>

Use functionality from NuGet packages

User Scenario: As a generator author I want to rely on functionality provided in NuGet packages inside my generator.

Solution: It is possible to depend on NuGet packages inside of a generator, but special consideration has to be taken for distribution.

Any runtime dependencies, that is, code that the end users program will need to rely on, can simply be added as a dependency of the generator NuGet package via the usual referencing mechanism.

For example, consider a generator that creates code that relies on Newtonsoft.Json. The generator does not directly use the dependency, it just emits code that relies on the library being referenced in the users compilation. The author would add a reference to Newtonsoft.Json as a public dependency, and when the user adds the generator package it will be referenced automatically.

xml
<Project>
  <PropertyGroup>
    <GeneratePackageOnBuild>true</GeneratePackageOnBuild> 
    <IncludeBuildOutput>false</IncludeBuildOutput> 
  </PropertyGroup>

  <ItemGroup>
    
    <PackageReference Include="Newtonsoft.Json" Version="12.0.1" />

    
    <None Include="$(OutputPath)\$(AssemblyName).dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
  </ItemGroup>
</Project>

However, any generation-time dependencies, that is, used by the generator while it is is running and generating code, must be packaged directly alongside the generator assembly inside the generator NuGet package. There are no automatic facilities for this, and you will need to manually specify the dependencies to include.

Consider a generator that uses Newtonsoft.Json to encode something to json during the generation pass, but does not emit any code the relies on it being present at runtime. The author would add a reference to Newtonsoft.Json but make all of its assets private; this ensures the consumer of the generator does not inherit a dependency on the library.

The author would then have to package the Newtonsoft.Json library alongside the generator inside of the NuGet package. This can be achieved in the following way: set the dependency to generate a path property by adding GeneratePathProperty="true". This will create a new MSBuild property of the format PKG where `` is the package name with . replaced by _. In our example there would be an MSBuild property called PKGNewtonsoft_Json with a value that points to the path on disk of the binary contents of the NuGet files. We can then use that to add the binaries to the resulting NuGet package as we do with the generator itself:

xml
<Project>
  <PropertyGroup>
    <GeneratePackageOnBuild>true</GeneratePackageOnBuild> 
    <IncludeBuildOutput>false</IncludeBuildOutput> 
  </PropertyGroup>

  <ItemGroup>
    
    <PackageReference Include="Newtonsoft.Json" Version="12.0.1" PrivateAssets="all" GeneratePathProperty="true" />

    
    <None Include="$(OutputPath)\$(AssemblyName).dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />

    
    <None Include="$(PkgNewtonsoft_Json)\lib\netstandard2.0\*.dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
  </ItemGroup>
</Project>
c#
[Generator]
public class JsonUsingGenerator : IIncrementalGenerator
{
    public void Initialize(IncrementalGeneratorInitializationContext context)
    {
        var pipeline = context.AdditionalTextsProvider.Select(static (text, cancellationToken) =>
        {
            if (!text.Path.EndsWith("*.json"))
            {
                return default;
            }

            return (Name: Path.GetFileName(text.Path), Value: Newtonsoft.Json.JsonConvert.DeserializeObject<MyObject>(text.GetText(cancellationToken).ToString()));
        })
        .Where((pair) => pair is not ((_, null) or (null, _)));

        context.RegisterSourceOutput(pipeline, static (context, pair) =>
        {
            var sourceText = SourceText.From($$"""
                namespace GeneratedNamespace
                {
                    internal sealed class GeneratedClass
                    {
                        public static const (int A, int B) SerializedContent = ({{pair.A}}, {{pair.B}});
                    }
                }
                """, Encoding.UTF8);

            context.AddSource($"{pair.Name}generated.cs", sourceText)
        });
    }

    record MyObject(int A, int B);
}

Access Analyzer Config properties

User Scenarios:

  • As a generator author I want to access the analyzer config properties for a syntax tree or additional file.
  • As a generator author I want to access key-value pairs that customize the generator output.
  • As a user of a generator I want to be able to customize the generated code and override defaults.

Solution: Generators can access analyzer config values via the AnalyzerConfigOptionsProvider. Analyzer config values can either be accessed in the context of a SyntaxTree, AdditionalFile or globally via GlobalOptions. Global options are 'ambient' in that they don't apply to any specific context, but will be included when requesting option within a specific context.

Note that this is one of the few cases that it is necessary to put a SyntaxNode into a pipeline, as you need the tree in order to get the generator option.
Try to get the SyntaxNode out of the pipeline as fast as possible to avoid making the model not correctly equatable.

A generator is free to use a global option to customize its output. For example, consider a generator that can optionally emit logging. The author may choose to check the value of a global analyzer config value in order to control whether or not to emit the logging code. A user can then choose to enable the setting per project via an .globalconfig file:

.globalconfig
mygenerator_emit_logging = true
csharp
[Generator]
public class MyGenerator : IIncrementalGenerator
{
    public void Initialize(IncrementalGeneratorInitializationContext context)
    {

        var userCodePipeline = context.SyntaxProvider.ForAttributeWithMetadataName(... /* collect user code info */);
        var emitLoggingPipeline = context.AnalyzerConfigOptionsProvider.Select(static (options, cancellationToken) =>
            options.GlobalOptions.TryGetValue("mygenerator_emit_logging", out var emitLoggingSwitch)
                ? emitLoggingSwitch.Equals("true", StringComparison.InvariantCultureIgnoreCase)
                : false); // Default

        context.RegisterSourceOutput(userCodePipeline.Combine(emitLoggingPipeline), (context, pair) => /* emit code */);
    }
}

Consume MSBuild properties and metadata

User Scenarios:

  • As a generator author I want to make decisions based on the values contained in the project file
  • As a user of a generator I want to be able to customize the generated code and override defaults.

Solution: MSBuild will automatically translate specified properties and metadata into a global analyzer config that can be read by a generator. A generator author specifies the properties and metadata they want to make available by adding items to the CompilerVisibleProperty and CompilerVisibleItemMetadata item groups. These can be added via a props or targets file when packaging the generator as a NuGet package.

For example, consider a generator that creates source based on additional files, and wants to allow a user to enable or disable logging via the project file. The author would specify in their props file that they want to make the specified MSBuild property visible to the compiler:

xml
<ItemGroup>
    <CompilerVisibleProperty Include="MyGenerator_EnableLogging" />
</ItemGroup>

The value of MyGenerator_EnableLogging property will then be emitted to a generated analyzer config file before build, with a name of build_property.MyGenerator_EnableLogging. The generator is then able read this property from via the GlobalOptions property of the AnalyzerConfigOptionsProvider pipeline:

c#
context.AnalyzerConfigOptionsProvider.Select((provider, ct) =>
    provider.GlobalOptions.TryGetValue("build_property.MyGenerator_EnableLogging", out var emitLoggingSwitch)
        ? emitLoggingSwitch.Equals("true", StringComparison.InvariantCultureIgnoreCase) : false);

A user can thus enable, or disable logging, by setting a property in their project file.

Now, consider that the generator author wants to optionally allow opting in/out of logging on a per-additional file basis. The author can request that MSBuild emit the value of metadata for the specified file, by adding to the CompilerVisibleItemMetadata item group. The author specifies both the MSBuild itemType they want to read the metadata from, in this case AdditionalFiles, and the name of the metadata that they want to retrieve for them.

xml
<ItemGroup>
    <CompilerVisibleItemMetadata Include="AdditionalFiles" MetadataName="MyGenerator_EnableLogging" />
</ItemGroup>

This value of MyGenerator_EnableLogging will be emitted to a generated analyzer config file, for each of the additional files in the compilation, with an item name of build_metadata.AdditionalFiles.MyGenerator_EnableLogging. The generator can read this value in the context of each additional file:

cs
context.AdditionalTextsProvider
       .Combine(context.AnalyzerConfigOptionsProvider)
       .Select((pair, ctx) =>
           pair.Right.GetOptions(pair.Left).TryGetValue("build_metadata.AdditionalFiles.MyGenerator_EnableLogging", out var perFileLoggingSwitch)
               ? perFileLoggingSwitch : false);

In the users project file, the user can now annotate the individual additional files to say whether or not they want to enable logging:

xml
<ItemGroup>
    <AdditionalFiles Include="file1.txt" />  
    <AdditionalFiles Include="file2.txt" MyGenerator_EnableLogging="true" />  
    <AdditionalFiles Include="file3.txt" MyGenerator_EnableLogging="false" /> 
</ItemGroup>

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. 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
<Project>
    <ItemGroup>
        <CompilerVisibleProperty Include="_MyInterpolatorsNamespaces" />
    </ItemGroup>
    <Task Name="_MyInterpolatorsNamespaces" BeforeTargets="BeforeBuild">
        <PropertyGroup>
            <_MyInterpolatorsNamespaces>$([System.String]::Copy('$(InterpolatorsNamespaces)').Replace(';', ' '))</_MyInterpolatorsNamespaces>
        </PropertyGroup>
    </Task>
</Project>

Full Example:

MyGenerator.props:

xml
<Project>
    <ItemGroup>
        <CompilerVisibleProperty Include="MyGenerator_EnableLogging" />
        <CompilerVisibleItemMetadata Include="AdditionalFiles" MetadataName="MyGenerator_EnableLogging" />
    </ItemGroup>
</Project>

MyGenerator.csproj:

xml
<Project>
  <PropertyGroup>
    <GeneratePackageOnBuild>true</GeneratePackageOnBuild> 
    <IncludeBuildOutput>false</IncludeBuildOutput> 
  </PropertyGroup>

  <ItemGroup>
    
    <None Include="$(OutputPath)\$(AssemblyName).dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />

    
    <None Include="MyGenerator.props" Pack="true" PackagePath="build" Visible="false" />
  </ItemGroup>
</Project>

MyGenerator.cs:

csharp
[Generator]
public class MyGenerator : IIncrementalGenerator
{
    public void Initialize(IncrementalGeneratorInitializationContext context)
    {
        var emitLoggingPipeline = context.AdditionalTextsProvider
            .Combine(context.AnalyzerConfigOptionsProvider)
            .Select((pair, ctx) =>
                pair.Right.GetOptions(pair.Left).TryGetValue("build_metadata.AdditionalFiles.MyGenerator_EnableLogging", out var perFileLoggingSwitch)
                ? perFileLoggingSwitch.Equals("true", StringComparison.OrdinalIgnoreCase)
                : pair.Right.GlobalOptions.TryGetValue("build_property.MyGenerator_EnableLogging", out var emitLoggingSwitch)
                  ? emitLoggingSwitch.Equals("true", StringComparison.OrdinalIgnoreCase)
                  : false);

        var sourcePipeline = context.AdditionalTextsProvider.Select((file, ctx) => /* Gather build info */);

        context.RegisterSourceOutput(sourcePipeline.Combine(emitLoggingPipeline), (context, pair) => /* Add source */);
    }
}

Unit Testing of Generators

User scenario: As a generator author, I want to be able to unit test my generators to make development easier and ensure correctness.

Solution A:

The recommended approach is to use Microsoft.CodeAnalysis.Testing packages:

  • Microsoft.CodeAnalysis.CSharp.SourceGenerators.Testing
  • Microsoft.CodeAnalysis.VisualBasic.SourceGenerators.Testing

TODO: https://github.com/dotnet/roslyn/issues/72149

Auto interface implementation

User scenario: As a generator author I want to be able to implement the properties of interfaces passed as arguments to a decorator of a class automatically for a user

Solution: Require the user to decorate the class with the [AutoImplement] Attribute and pass as arguments the types of the interfaces they want to self-implement themselves; The classes that implement the attribute have to be partial class.
Provide that attribute in a RegisterPostInitializationOutput step. Register for callbacks on the classes with
ForAttributeWithMetadataName using the fullyQualifiedMetadataName FullyQualifiedAttributeName, and use tuples (or create an equatable model) to pass along that information.
The attribute could work for structs too, the example was kept simple on purpose for the workbook sample.

Example:

csharp
public interface IUserInterface
{
    int InterfaceProperty { get; set; }
}

public interface IUserInterface2
{
    float InterfacePropertyOnlyGetter { get; }
}

[AutoImplementProperties(typeof(IUserInterface), typeof(IUserInterface2))]
public partial class UserClass
{
    public string UserProp { get; set; }
}
text
/* Detailed source-code truncated for AI context efficiency. */

Breaking Changes:

  • None currently

Open Issues

This section track other miscellaneous TODO items:

Framework targets: May want to mention if we have framework requirements for the generators, e.g. they must target netstandard2.0 or similar.

Conventions: (See TODO in conventions section above). What standard conventions are we suggesting to users?

Feature detection: Show how to create a generator that relies on specific target framework features, without depending on the TargetFramework property.


Features Incremental Generators (`docs/features/incremental-generators.md`)

Incremental Generators

Summary

Incremental generators are a new API that replaces
v1 source generators to allow users to specify generation
strategies that can be applied in a high performance way by the hosting layer.

High Level Design Goals

  • Allow for a finer grained approach to defining a generator
  • Scale source generators to support 'Roslyn/CoreCLR' scale projects in Visual Studio
  • Exploit caching between fine grained steps to reduce duplicate work
  • Support generating more items than just source texts
  • Exist alongside ISourceGenerator based implementations

Simple Example

We begin by defining a simple incremental generator that extracts the contents
of additional text files and makes their contents available as compile time
consts. In the following section we'll go into more depth around the concepts
shown.

csharp
[Generator]
public class Generator : IIncrementalGenerator
{
    public void Initialize(IncrementalGeneratorInitializationContext initContext)
    {
        // define the execution pipeline here via a series of transformations:

        // find all additional files that end with .txt
        IncrementalValuesProvider<AdditionalText> textFiles = initContext.AdditionalTextsProvider.Where(static file => file.Path.EndsWith(".txt"));

        // read their contents and save their name
        IncrementalValuesProvider<(string name, string content)> namesAndContents = textFiles.Select((text, cancellationToken) => (name: Path.GetFileNameWithoutExtension(text.Path), content: text.GetText(cancellationToken)!.ToString()));

        // generate a class that contains their values as const strings
        initContext.RegisterSourceOutput(namesAndContents, (spc, nameAndContent) =>
        {
            spc.AddSource($"ConstStrings.{nameAndContent.name}", $@"
    public static partial class ConstStrings
    {{
        public const string {nameAndContent.name} = ""{nameAndContent.content}"";
    }}");
        });
    }
}

Implementation

An incremental generator is an implementation of Microsoft.CodeAnalysis.IIncrementalGenerator.

csharp
namespace Microsoft.CodeAnalysis
{
    public interface IIncrementalGenerator
    {
        void Initialize(IncrementalGeneratorInitializationContext initContext);
    }
}

As with source generators, incremental generators are defined in external
assemblies and passed to the compiler via the -analyzer: option.
Implementations are required to be annotated with the
Microsoft.CodeAnalysis.GeneratorAttribute with an optional parameter
indicating the languages the generator supports:

csharp
[Generator(LanguageNames.CSharp)]
public class MyGenerator : IIncrementalGenerator { ... }

An assembly can contain a mix of diagnostic analyzers, source generators and
incremental generators.

Pipeline based execution

IIncrementalGenerator has an Initialize method that is called by the
host[^1] exactly once, regardless of the number of further compilations that may
occur. For instance a host with multiple loaded projects may share the same
generator instance across multiple projects, and will only call Initialize a
single time for the lifetime of the host.

[^1]: Such as the IDE or the command-line compiler

Rather than a dedicated Execute method, an Incremental Generator instead
defines an immutable execution pipeline as part of initialization. The
Initialize method receives an instance of
IncrementalGeneratorInitializationContext which is used by the generator to
define a set of transformations.

csharp
public void Initialize(IncrementalGeneratorInitializationContext initContext)
{
    // define the execution pipeline here via a series of transformations:
}

The defined transformations are not executed directly at initialization, and
instead are deferred until the data they are using changes. Conceptually this is
similar to LINQ, where a lambda expression might not be executed until the
enumerable is actually iterated over:

IEnumerable:

csharp
var squares = Enumerable.Range(1, 10).Select(i => i * i); 

    // the code inside select is not executed until we iterate the collection
    foreach (var square in squares) { ... }

These transformations are used to form a directed graph of actions that can be
executed on demand later, as the input data changes.

Incremental Generators:

csharp
IncrementalValuesProvider<AdditionalText> textFiles = context.AdditionalTextsProvider.Where(static file => file.Path.EndsWith(".txt"));
    // the code in the Where(...) above will not be executed until the value of the additional texts actually changes

Between each transformation, the data produced is cached, allowing previously calculated
values to be re-used where applicable. This caching reduces the computation
required for subsequent compilations. See caching for more details.

IncrementalValue\[s\]Provider<T>

Input data is available to the pipeline in the form of opaque data sources,
either an IncrementalValueProvider<T> or IncrementalValuesProvider<T> (note
the plural values) where T is the type of input data that is provided.

An initial set of providers are created by the host, and can be accessed from the
IncrementalGeneratorInitializationContext provided during initialization.

The currently available providers are:

  • CompilationProvider
  • AdditionalTextsProvider
  • AnalyzerConfigOptionsProvider
  • MetadataReferencesProvider
  • ParseOptionsProvider

Note: there is no provider for accessing syntax nodes. This is handled
in a slightly different way. See SyntaxValueProvider for details.

A value provider can be thought of as a 'box' that holds the value itself. An
execution pipeline does not access the values in a value provider directly.

ascii
IValueProvider<TSource>
   ┌─────────────┐
   |             |
   │   TSource   │
   |             |
   └─────────────┘

Instead, the generator supplies a set of transformations that are to be applied to the
data contained within the provider, which in turn creates a new value provider.

Select

The simplest transformation is Select. This maps the value in one provider
into a new provider by applying a transform to it.

ascii
IValueProvider<TSource>                   IValueProvider<TResult>
    ┌─────────────┐                           ┌─────────────┐
    │             │  Select<TSource,TResult>  │             │
    │   TSource   ├──────────────────────────►│   TResult   │
    │             │                           │             │
    └─────────────┘                           └─────────────┘

Generator transformations can be thought of as being conceptually somewhat similar to
LINQ, with the value provider taking the place of IEnumerable<T>.
Transforms are created through a set of extension methods:

csharp
public static partial class IncrementalValueSourceExtensions
{
    // 1 => 1 transform 
    public static IncrementalValueProvider<TResult> Select<TSource, TResult>(this IncrementalValueProvider<TSource> source, Func<TSource, CancellationToken, TResult> selector);
    public static IncrementalValuesProvider<TResult> Select<TSource, TResult>(this IncrementalValuesProvider<TSource> source, Func<TSource, CancellationToken, TResult> selector);
}

Note how the return type of these methods are also an instance of
IncrementalValue[s]Provider. This allows the generator to chain multiple
transformations together:

ascii
IValueProvider<TSource>                     IValueProvider<TResult1>                 IValueProvider<TResult2>
    ┌─────────────┐                            ┌─────────────┐                           ┌─────────────┐
    │             │  Select<TSource,TResult1>  │             │ Select<TResult1,TResult2> │             │
    │   TSource   ├───────────────────────────►│   TResult1  │──────────────────────────►│   TResult2  │
    │             │                            │             │                           │             │
    └─────────────┘                            └─────────────┘                           └─────────────┘

Consider the following simple example:

csharp
// get the additional text provider
IncrementalValuesProvider<AdditionalText> additionalTexts = initContext.AdditionalTextsProvider;

// apply a 1-to-1 transform on each text, which represents extracting the path
IncrementalValuesProvider<string> transformed = additionalTexts.Select(static (text, _) => text.Path);

// transform each extracted path into something else
IncrementalValuesProvider<string> prefixTransform = transformed.Select(static (path, _) => "prefix_" + path);

Note how transformed and prefixTransform are themselves an
IncrementalValuesProvider. They represent the outcome of the transformation
that will be applied, rather than the resulting data.

Multi Valued providers

An IncrementalValueProvider<T> will always provide a single value, whereas an
IncrementalValuesProvider<T> may provide zero or more values. For example the
CompilationProvider will always produce a single compilation instance, whereas
the AdditionalTextsProvider will produce a variable number of values,
depending on how many additional texts were passed to the compiler.

Conceptually it is simple to think about the transformation of a single item
from an IncrementalValueProvider<T>: the single item has the selector function
applied to it which produces a single value of TResult.

For an IncrementalValuesProvider<T> however, this transformation is more
subtle. The selector function is applied multiple times, one to each item in the
values provider. The results of each transformation are then used to create the
values for the resulting values provider:

text
/* Detailed source-code truncated for AI context efficiency. */

It is this item-wise transformation that allows the caching to be particularly
powerful in this model. Consider when the values inside
IncrementalValueProvider<TSource> change. Its likely that any given change
will only change one item at a time rather than the whole collection (for example
a user typing in an additional text only changes the given text, leaving the
other additional texts unmodified).

When this occurs the generator driver can compare the input items with the ones
that were used previously. If they are considered to be equal then the
transformations for those items can be skipped and the previously computed
versions used instead. See Comparing Items for more details.

In the above diagram if Item2 were to change we would execute the selector on
the modified value producing a new value for Result2. As Item1and Item3
are unchanged the driver is free to skip executing the selector and just use the
cached values of Result1 and Result3 from the previous execution.

Select Many

In addition to the 1-to-1 transform shown above, there are also transformations
that produce batches of data. For instance a given transformation may want to
produce multiple values for each input. There are a set of SelectMany methods that allow a transformation of 1 to
many, or many to many items:

1 to many:

csharp
public static partial class IncrementalValueSourceExtensions
{
    public static IncrementalValuesProvider<TResult> SelectMany<TSource, TResult>(this IncrementalValueProvider<TSource> source, Func<TSource, CancellationToken, IEnumerable<TResult>> selector);
}
text
/* Detailed source-code truncated for AI context efficiency. */

Many to many:

csharp
public static partial class IncrementalValueSourceExtensions
{
    public static IncrementalValuesProvider<TResult> SelectMany<TSource, TResult>(this IncrementalValuesProvider<TSource> source, Func<TSource, CancellationToken, IEnumerable<TResult>> selector);
}
text
/* Detailed source-code truncated for AI context efficiency. */

For example, consider a set of additional XML files that contain multiple
elements of the same type. The generator may want to treat each element as a
distinct item for generation, effectively splitting a single additional file
into multiple sub-items.

csharp
// get the additional text provider
IncrementalValuesProvider<AdditionalText> additionalTexts = initContext.AdditionalTextsProvider;

// extract each element from each additional file
IncrementalValuesProvider<MyElementType> elements = additionalTexts.SelectMany(static (text, _) => /*transform text into an array of MyElementType*/);

// now the generator can consider the union of elements in all additional texts, without needing to consider multiple files
IncrementalValuesProvider<string> transformed = elements.Select(static (element, _) => /*transform the individual element*/);

Where

Where allows the author to filter the values in a value provider by a given
predicate. Where is actually a specific form of select many, where each input
transforms to exactly 1 or 0 outputs. However, as it is such a common operation
it is provided as a primitive transformation directly.

csharp
public static partial class IncrementalValueSourceExtensions
{
    public static IncrementalValuesProvider<TSource> Where<TSource>(this IncrementalValuesProvider<TSource> source, Func<TSource, bool> predicate);
}
text
/* Detailed source-code truncated for AI context efficiency. */

An obvious use case is to filter out inputs the generator knows it isn't
interested in. For example, the generator will likely want to filter additional
texts on file extensions:

csharp
// get the additional text provider
IncrementalValuesProvider<AdditionalText> additionalTexts = initContext.AdditionalTextsProvider;

// filter additional texts by extension
IncrementalValuesProvider<string> xmlFiles = additionalTexts.Where(static (text, _) => text.Path.EndsWith(".xml", StringComparison.OrdinalIgnoreCase));

Collect

When performing transformations on a value provider with multiple items, it can
often be useful to view the items as a single collection rather than one item at
a time. For this there is the Collect transformation.

Collect transforms an IncrementalValuesProvider<T> to an
IncrementalValueProvider>. Essentially it transforms a multi-valued source
into a single value source with an array of all the items.

csharp
public static partial class IncrementalValueSourceExtensions
{
    IncrementalValueProvider<ImmutableArray<TSource>> Collect<TSource>(this IncrementalValuesProvider<TSource> source);
}
ascii
IncrementalValuesProvider<TSource>                IncrementalValueProvider<ImmutableArray<TSource>>
          ┌───────────┐                                  ┌─────────────────────────┐
          │           │          Collect<TSource>        │                         │
          │  TSource  ├─────────────────────────────────►│ ImmutableArray<TSource> │
          │           │                                  │                         │
          └───────────┘                                  └─────────────────────────┘
             3 Items                                             Single Item

              Item1                                         [Item1, Item2, Item3]
              Item2
              Item3
csharp
// get the additional text provider
IncrementalValuesProvider<AdditionalText> additionalTexts = initContext.AdditionalTextsProvider;

// collect the additional texts into a single item
IncrementalValueProvider<AdditionalText[]> collected = additionalTexts.Collect();

// perform a transformation where you can access all texts at once
var transform = collected.Select(static (texts, _) => /* ... */);

Multi-path pipelines

The transformations described so far are all effectively single-path operations:
while there may be multiple items in a given provider, each transformation
operates on a single input value provider and produces a single derived output
provider.

While sufficient for simple operations, it is often necessary to combine the
values from multiple input providers or use the results of a transformation
multiple times. For this there are a set of transformations that split and
combine a single path of transformations into a multi-path pipeline.

Split

It is possible to split the output of a transformation into multiple
parallel inputs. Rather than having a dedicated transformation this can be
achieved by simply using the same value provider as the input to multiple
transforms.

ascii
IncrementalValueProvider<TResult>
                                                              ┌───────────┐
                                      Select<TSource,TResult> │           │
 IncrementalValueProvider<TSource>   ┌───────────────────────►│  TResult  │
           ┌───────────┐             │                        │           │
           │           │             │                        └───────────┘
           │  TSource  ├─────────────┤
           │           │             │
           └───────────┘             │                    IncrementalValuesProvider<TResult2>
                                     │                              ┌───────────┐
                                     │ SelectMany<TSource,TResult2> │           │
                                     └─────────────────────────────►│  TResult2 │
                                                                    │           │
                                                                    └───────────┘

Those transforms can then be used as the inputs to new single path transforms, independent of one another.

For example:

csharp
// get the additional text provider
IncrementalValuesProvider<AdditionalText> additionalTexts = context.AdditionalTextsProvider;

// apply a 1-to-1 transform on each text, extracting the path
IncrementalValuesProvider<string> transformed = additionalTexts.Select(static (text, _) => text.Path);

// split the processing into two paths of derived data
IncrementalValuesProvider<string> nameTransform = transformed.Select(static (path, _) => "prefix_" + path);
IncrementalValuesProvider<string> extensionTransform = transformed.Select(static (path, _) => Path.ChangeExtension(path, ".new"));

nameTransform and extensionTransform produce different values for the same
set of additional text inputs. For example if there was an additional file
called file.txt then nameTransform would produce the string
prefix_file.txt where extensionTransform would produce the string
file.new.

When the value of the additional file changes, the subsequent values produced
may or may not differ. For example if the name of the additional file was
changed to file.xml then nameTransform would now produce prefix_file.xml
whereas extensionTransform would still produce file.new. Any child transform
with input from nameTransform would be re-run with the new value, but any
child of extensionTransform would use the previously cached version as it's
input hasn't changed.

Combine

Combine is the most powerful, but also most complicated transformation. It
allows a generator to take two input providers and create a single unified
output provider.

Single-value to single-value:

csharp
public static partial class IncrementalValueSourceExtensions
{
    IncrementalValueProvider<(TLeft Left, TRight Right)> Combine<TLeft, TRight>(this IncrementalValueProvider<TLeft> provider1, IncrementalValueProvider<TRight> provider2);
}

When combining two single value providers, the resulting node is conceptually
easy to understand: a new value provider that contains a Tuple of the two
input items.

ascii
IncrementalValueProvider<TSource1>
         ┌───────────┐
         │           │
         │  TSource1 ├────────────────┐
         │           │                │                                 IncrementalValueProvider<(TSource1, TSource2)>
         └───────────┘                │
          Single Item                 │                                          ┌────────────────────────┐
                                      │       Combine<TSource1, TSource2>        │                        │
            Item1                     ├─────────────────────────────────────────►│  (TSource1, TSource2)  │
                                      │                                          │                        │
IncrementalValueProvider<TSource2>    │                                          └────────────────────────┘
         ┌───────────┐                │                                                   Single Item
         │           │                │
         │  TSource2 ├────────────────┘                                                  (Item1, Item2)
         │           │
         └───────────┘
          Single Item

            Item2

Multi-value to single-value:

csharp
public static partial class IncrementalValueSourceExtensions
{
    IncrementalValuesProvider<(TLeft Left, TRight Right)> Combine<TLeft, TRight>(this IncrementalValuesProvider<TLeft> provider1, IncrementalValueProvider<TRight> provider2);
}

When combining a multi value provider to a single value provider, however, the
semantics are a little more complicated. The resulting multi-valued provider
produces a series of tuples: the left hand side of each tuple is the value
produced from the multi-value input, while the right hand side is always the
same single value from the single value provider input.

ascii
IncrementalValuesProvider<TSource1>
          ┌───────────┐
          │           │
          │  TSource1 ├────────────────┐
          │           │                │
          └───────────┘                │
             3 Items                   │                                IncrementalValuesProvider<(TSource1, TSource2)>
                                       │
            LeftItem1                  │                                          ┌────────────────────────┐
            LeftItem2                  │       Combine<TSource1, TSource2>        │                        │
            LeftItem3                  ├─────────────────────────────────────────►│  (TSource1, TSource2)  │
                                       │                                          │                        │
                                       │                                          └────────────────────────┘
 IncrementalValueProvider<TSource2>    │                                                  3 Items
          ┌───────────┐                │
          │           │                │                                            (LeftItem1, RightItem)
          │  TSource2 ├────────────────┘                                            (LeftItem2, RightItem)
          │           │                                                             (LeftItem3, RightItem)
          └───────────┘
           Single Item

            RightItem

Multi-value to multi-value:

As shown by the definitions above it is not possible to combine a multi-value
source to another multi-value source. The resulting cross join would potentially
contain a large number of values, so the operation is not provided by default.

Instead, an author can call Collect() on one of the input multi-value providers
to produce a single-value provider that can be combined as above.

text
/* Detailed source-code truncated for AI context efficiency. */

With the above transformations the generator author can now take one or more
inputs and combine them into a single source of data. For example:

csharp
// get the additional text provider
IncrementalValuesProvider<AdditionalText> additionalTexts = initContext.AdditionalTextsProvider;

// combine each additional text with the parse options
IncrementalValuesProvider<(AdditionalText, ParseOptions)> combined = additionalTexts.Combine(initContext.ParseOptionsProvider);

// perform a transform on each text, with access to the options
var transformed = combined.Select(static (pair, _) => 
{
    AdditionalText text = pair.Left;
    ParseOptions parseOptions = pair.Right;
    // do the actual transform ...
});

If either of the inputs to a combine change, then subsequent transformation will
re-run. However, the caching is considered on a pairwise basis for each output
tuple. For instance, in the above example, if only additional text changes the
subsequent transform will only be run for the text that changed. The other text
and parse options pairs are skipped and their previously computed value are
used. If the single value changes, such as the parse options in the example,
then the transformation is executed for every tuple.

SyntaxValueProvider

Syntax Nodes are not available directly through a value provider. Instead, a
generator author uses the special SyntaxValueProvider (provided via the
IncrementalGeneratorInitializationContext.SyntaxProvider) to create a
dedicated input node that instead exposes a sub-set of the syntax they are
interested in. The syntax provider is specialized in this way to achieve a
desired level of performance.

CreateSyntaxProvider

Currently the provider exposes a single method CreateSyntaxProvider that
allows the author to construct an input node.

csharp
public readonly struct SyntaxValueProvider
    {
        public IncrementalValuesProvider<T> CreateSyntaxProvider<T>(Func<SyntaxNode, CancellationToken, bool> predicate, Func<GeneratorSyntaxContext, CancellationToken, T> transform);
    }

Note how this takes two lambda parameters: one that examines a SyntaxNode in
isolation, and a second one that can then use the GeneratorSyntaxContext to
access a semantic model and transform the node for downstream usage.

It is because of this split that performance can be achieved: as the driver is
aware of which nodes are chosen for examination, it can safely skip the first
predicate lambda when a syntax tree remains unchanged. The driver will still
re-run the second transform lambda even for nodes in unchanged files, as a
change in one file can impact the semantic meaning of a node in another file.

Consider the following syntax trees:

csharp
// file1.cs
public class Class1
{
    public int Method1() => 0;
}

// file2.cs
public class Class2
{
    public Class1 Method2() => null;
}

// file3.cs
public class Class3 {}

As an author I can make an input node that extracts the return type information

csharp
// create a syntax provider that extracts the return type kind of method symbols
var returnKinds = initContext.SyntaxProvider.CreateSyntaxProvider(static (n, _) => n is MethodDeclarationSyntax,
                                                                  static (n, _) => ((IMethodSymbol)n.SemanticModel.GetDeclaredSymbol(n.Node)).ReturnType.Kind);

Initially the predicate will run for all syntax nodes, and select the two
MethodDeclarationSyntax nodes Method1() and Method2(). These are then
passed to the transform where the semantic model is used to obtain the method
symbol and extract the kind of the return type for the method. returnKinds
will contain two values, both NamedType.

Now imagine that file3.cs is edited:

csharp
// file3.cs
public class Class3 {
    public int field;
}

The predicate will only be run for syntax nodes inside file3.cs, and will
not return any as it still doesn't contain any method symbols. The transform
however will still be run again for the two methods from Class1 and Class2.

To see why it was necessary to re-run the transform consider the following
edit to file1.cs where we change the class's name:

csharp
// file1.cs
public class Class4
{
    public int Method1() => 0;
}

The predicate will be re-run for file1.cs as it has changed, and will pick
out the method symbol Method1() again. Next, because the transform is
re-run for all the methods, the return type kind for Method2() is correctly
changed to ErrorType as Class1 no longer exists.

Note that we didn't need to run the predicate over for nodes in file2.cs
even though they referenced something in file1.cs. Because the first check is
purely syntactic we can be sure the results for file2.cs would be the same.

While it may seem unfortunate that the driver must run the transform for all
selected syntax nodes, if it did not it could end up producing incorrect data
due to cross file dependencies. Because the initial syntactic check
allows the driver to substantially filter the number of nodes on which the
semantic checks have to be re-run, significantly improved performance
characteristics are still observed when editing a syntax tree.

ForAttributeWithMetadataName (FAWMN)

One extremely common action we observe generators being written for is taking
actions driven on attributes applied to specific syntax constructs.

csharp
public readonly struct SyntaxValueProvider
{
    public IncrementalValuesProvider<T> ForAttributeWithMetadataName<T>(
        string fullyQualifiedMetadataName,
        Func<SyntaxNode, CancellationToken, bool> predicate,
        Func<GeneratorAttributeSyntaxContext, CancellationToken, T> transform);
}

This area is particularly nice for optimization, as we can efficiently eliminate
a significant number of syntax nodes and edits before even needing to call the
provided predicate from the user, avoiding realizing a significant number of
SyntaxNode instances. Roslyn can even further optimize this by tracking whether or
not a given attribute could possibly be the attribute the generator cares about by
maintaining a small index and comparing type names as an initial heuristic.
This index is cheap to maintain and, importantly, can only have false positives, not
false negatives. This allows us to eliminate 99% of syntax in a Compilation from ever
needing to be checked for semantic information (to eliminate false positives from the
heuristic cache) or by the user predicate function (saving a significant number of
allocations of SyntaxNode instances).

Given this, when at all possible, it is recommended to use attributes to drive source
generators, rather than other syntax constructs. Real world testing has indicated this
approach is usually 99x more efficient than CreateSyntaxProvider, even when the
generator is otherwise not well-behaved; some pathological scenarios are even more efficient
than that.

Attributes are provided by the user as the fully-qualified metadata name, without the
assembly name portion. For example, given the C# type My.Namespace.MyAttribute<T>,
the fully-qualified metadata-name would be My.Namespace.MyAttribute`1. Given that
attributes are usually restricted to specific constructs by an AttributeUsage
attribute, it is common that the predicate a user provides will simply return true.
For the transformation step, everything stated in the previous section
is still relevant; that step will still be rerun with every change to ensure that changed
semantics are observed.

Outputting values

At some point in the pipeline the author will want to actually use the
transformed data to produce an output, such as a SourceText. There are a set
of Register...Output methods on the
IncrementalGeneratorInitializationContext that allow the generator author to
construct an output from a series of transformations.

These output registrations are terminal, in that the they do not return a value
provider and can have no further transformations applied to them. However an
author is free to register multiple outputs of the same type with different
input transformations.

The set of output methods are

  • RegisterSourceOutput
  • RegisterImplementationSourceOutput
  • RegisterPostInitializationOutput

RegisterSourceOutput:

RegisterSourceOutput allows a generator author to produce source files and
diagnostics that will be included in the users compilation. As input, it takes a
Value[s]Provider and an Action that will
be invoked for every value in the value provider.

csharp
public static partial class IncrementalValueSourceExtensions
{
    public void RegisterSourceOutput<TSource>(IncrementalValueProvider<TSource> source, Action<SourceProductionContext, TSource> action);
    public void RegisterSourceOutput<TSource>(IncrementalValuesProvider<TSource> source, Action<SourceProductionContext, TSource> action);
}

The provided SourceProductionContext can be used to add source files and report diagnostics:

csharp
public readonly struct SourceProductionContext
{
    public CancellationToken CancellationToken { get; }

    public void AddSource(string hintName, string source);
   
    public void ReportDiagnostic(Diagnostic diagnostic);
}

For example, a generator can extract out the set of paths for the additional
files and create a method that prints them out:

csharp
// get the additional text provider
IncrementalValuesProvider<AdditionalText> additionalTexts = initContext.AdditionalTextsProvider;

// apply a 1-to-1 transform on each text, extracting the path
IncrementalValuesProvider<string> transformed = additionalTexts.Select(static (text, _) => text.Path);

// collect the paths into a batch
IncrementalValueProv