Adding a "fake attribute"

In this step I define a C# attribute [YacksSerialization()] in the YacksCore assembly, and then add some built-in support for this attribute to the Roslyn compiler. I'm calling this a "fake attribute" because it does not get emitted to the PE module, so it doesn't exist at runtime.

The intention behind this attribute is that it will be used to control some serialization / deserialization support that I intend to build in to Roslyn, but that willl first be done later. In this step I'm just modifying Roslyn to accept this attribute and record that it was used on a class or struct statement, and that's all.

Note: The code shown below is somewhat obsolete. A newer version is available for download - see this article.

YacksCore\YacksAttributes.cs

This is part of the YacksCore project, not a part of the Roslyn compiler project, although it does get included as a NuGet package. (The YacksCore project was previously mentioned here.)

////------------------------------------------------------------------------------------------ // // Merlinia Project Yacks // Copyright © Merlinia 2018, All Rights Reserved. // Licensed under the Apache License, Version 2.0. // (Just to be compatible with the Microsoft Roslyn license.) // ////------------------------------------------------------------------------------------------ using System; namespace Merlinia.Yacks { /// <summary> /// C# attribute that is used to specify that a class or enum or property or field is to be /// serializable using support provided by the Yacks-modified Roslyn compiler. /// /// This is still a work in progress, and details have not been finalized. /// /// Note that to some extent this is a "fake attribute", since the modified Roslyn compiler does /// not emit these attributes to the output PE module, and thus they are not accessible at /// runtime via reflection. /// /// The modified Roslyn compiler uses the information provided by this attribute for several /// purposes. In particular, it may be used to determine if a class needs to have a unique and /// persisted numeric object ID. /// /// The argument must be specified as a string. Sample arguments: /// /// [YacksSerialization("BSX")] /// /// B = Binary /// S = SOAP (limited support) /// X = XML /// /// This can be used on class and field and property and enum statements. When not used on a /// public field or property the parameters from the attribute on the class statement are /// used. /// /// Alternative form, only valid for public fields and properties: /// /// [YacksSerialization("")] /// /// This means no serialization for this field or property. /// </summary> [AttributeUsage(AttributeTargets.Class | AttributeTargets.Enum | AttributeTargets.Field | AttributeTargets.Struct | AttributeTargets.Property)] public sealed class YacksSerializationAttribute : Attribute { public string SerializationSpecifiers { get; } // Constructor public YacksSerializationAttribute(string serializationSpecifiers) { SerializationSpecifiers = serializationSpecifiers; } } }

src\Compilers\Core\Portable\Yacks_CompilationData.cs

This source file was previously mentioned here, although under a different name. Here's the current version, which now contains some string constants that are used to reference the [YacksSerialization()] attribute definition.

// Copyright (c) Merlinia A/S. All Rights Reserved. Licensed under the Apache License, Version 2.0. (Just to be compatible with the Microsoft Roslyn license.) using System.Collections.Concurrent; using System.Collections.Generic; using System.Globalization; using Merlinia.Yacks; namespace Microsoft.CodeAnalysis { /// <summary> /// This class contains some data associated with the Yacks modifications to the Roslyn compiler. /// It is only relevant for C# compilations, but because it is used in the CodeGenerator and /// MetadataWriter classes there is a reference to it in the Compilation class instead of the /// CSharpCompilation class. /// </summary> internal class Yacks_CompilationData { // Name of directory where anonymized output files are written internal const string CAnonymized = "Anonymized"; // Various usage of word "Merlinia" in emitted data internal const string CMerlinia = "Merlinia"; internal const string CMerliniaModule = CMerlinia + " module"; // Length of numeric part of a module name such as Merlinia1585 private const int CModuleNumberLength = 4; // Constant strings associated with processing of [YacksSerialization()] attributes internal static readonly string CYacksSerializationAttribute = typeof(YacksSerializationAttribute).Name; public static string CYacksSerialization = CYacksSerializationAttribute.Remove( CYacksSerializationAttribute.Length - "Attribute".Length); // Full path and file name of the LiteDB database YacksProjectDirectory.db file public string YacksProjectDirectory { get; set; } = null; // References to an Array and a Dictionary specifying the constant strings which should be // "disguised" (if the two references are not null). public string[] StringyNumberToString { get; internal set; } // Index is 1-based, not 0-based public Dictionary<string, int> StringToStringyNumber { get; internal set; } // Dictionary to convert a "stringy number" to the "fake token number" needed by the metadata // writer. public readonly ConcurrentDictionary<int, int> StringyNumberToFakeTokenNumber = new ConcurrentDictionary<int, int>(); // Yacks module number for this .Net assembly. This remains -1 if the YacksProjectDirectory // "fake property" has not been specified. public int ModuleNumber { get; set; } = -1; // Switch indicating that the YacksAnonymizeModule "fake property" was used public bool AnonymizeModule { get; set; } = false; // Switch that gets set when emitting the anonymized output file public bool EmittingAnonymized { get; set; } = false; /// <summary> /// Method to generate the assembly and module names used when the YacksAnonymizeModule option /// is on. /// </summary> public string GetModuleName() { return GetModuleName(ModuleNumber); } /// <summary> /// Method to generate assembly or module name used when the YacksAnonymizeModule option is /// on. /// </summary> public string GetModuleName(int moduleNumber) { return CMerlinia + moduleNumber.ToString("D" + CModuleNumberLength, CultureInfo.InvariantCulture); } } }

src\Compilers\CSharp\Portable\Declarations\DeclarationTreeBuilder.cs

This source file can be found in Visual Studio Solution Explorer under CSharpCodeAnalysis - Declarations. The class declaration was changed to include the partial keyword, and the following code was added and changed at the end of the VisitTypeDeclaration() method, which was at line 322 of the version of Roslyn I was working with.

//Yacks06: Process possible [YacksSerialization()] attribute SingleTypeDeclaration.FTypeFlagsForYacks yacksFlags = GetYacksSerializationAttribute(node, diagnostics); return new SingleTypeDeclaration( kind: kind, name: node.Identifier.ValueText, modifiers: modifiers, arity: node.Arity, declFlags: declFlags, yacksFlags: yacksFlags, // Added for Yacks step 06 syntaxReference: _syntaxTree.GetReference(node), nameLocation: new SourceLocation(node.Identifier), memberNames: memberNames, children: VisitTypeChildren(node), diagnostics: diagnostics.ToReadOnlyAndFree()); }

src\Compilers\CSharp\Portable\Declarations\DeclarationTreeBuilder.Yacks.cs

This is a new file that I added to the Roslyn project.

// Copyright (c) Merlinia A/S. All Rights Reserved. Licensed under the Apache License, Version 2.0. (Just to be compatible with the Microsoft Roslyn license.) using Microsoft.CodeAnalysis.CSharp.Syntax; namespace Microsoft.CodeAnalysis.CSharp { /// <summary> /// This source file contains some code that has been added to the DeclarationTreeBuilder class. /// But to keep the inline added code to a minimum the added code is placed in methods in this /// source file and called from the main DeclarationTreeBuilder.cs source file. /// </summary> partial class DeclarationTreeBuilder { /// <summary> /// Method to process a possible [YacksSerialization()] attribute on a type declaration. /// </summary> private static SingleTypeDeclaration.FTypeFlagsForYacks GetYacksSerializationAttribute(TypeDeclarationSyntax syntaxNode, DiagnosticBag diagnosticsBag) { foreach (AttributeListSyntax attributeListSyntax in syntaxNode.AttributeLists) { foreach (AttributeSyntax attributeSyntax in attributeListSyntax.Attributes) { IdentifierNameSyntax nameSyntax = attributeSyntax.Name as IdentifierNameSyntax; if (nameSyntax != null && nameSyntax.Identifier.Text == Yacks_CompilationData.CYacksSerialization) { SingleTypeDeclaration.FTypeFlagsForYacks flagsForYacks = SingleTypeDeclaration.FTypeFlagsForYacks.fNone; if (!GetYacksSerializationAttribute(attributeSyntax, ref flagsForYacks)) { diagnosticsBag.Add(ErrorCode.ERR_YacksBadSerializationAttribute, syntaxNode.GetLocation(), SyntaxFacts.GetText(syntaxNode.Kind())); } return flagsForYacks; } } } return SingleTypeDeclaration.FTypeFlagsForYacks.fNone; } /// <summary> /// Sub-method of above method to process a [YacksSerialization()] attribute on a type /// declaration. /// </summary> private static bool GetYacksSerializationAttribute(AttributeSyntax attributeSyntax, ref SingleTypeDeclaration.FTypeFlagsForYacks flagsForYacks) { if (attributeSyntax.ArgumentList.Arguments.Count != 1) return false; AttributeArgumentSyntax argumentSyntax = attributeSyntax.ArgumentList.Arguments[0]; if (argumentSyntax.Expression == null || argumentSyntax.Expression.Kind() != SyntaxKind.StringLiteralExpression) return false; LiteralExpressionSyntax literalExpression = argumentSyntax.Expression as LiteralExpressionSyntax; if (literalExpression == null) return false; string s = literalExpression.Token.ValueText; if (string.IsNullOrEmpty(s)) return false; foreach (char c in s) { switch (c) { case 'B': flagsForYacks |= SingleTypeDeclaration.FTypeFlagsForYacks.fSerializationBinary; break; case 'S': flagsForYacks |= SingleTypeDeclaration.FTypeFlagsForYacks.fSerializationSoap; break; case 'X': flagsForYacks |= SingleTypeDeclaration.FTypeFlagsForYacks.fSerializationXml; break; default: return false; } } return true; } } }

src\Compilers\CSharp\Portable\Errors\ErrorCode.cs

In Visual Studio Solution Explorer this file is under CSharpCodeAnalysis - Errors. One new error code was defined at the end of the file.

#region diagnostics added for Yacks modifications to Roslyn ERR_YacksBadSerializationAttribute = 42001, #endregion diagnostics added for Yacks modifications to Roslyn

src\Compilers\CSharp\Portable\CSharpResources.resx

The text for the new error code is added to this file. This can be done using a text editor, or else with the Visual Studio resource editor by double-clicking on the CSharpResources.resx file in Solution Explorer, where it is directly under the CSharpCodeAnalysis project.

<data name="ERR_YacksBadSerializationAttribute" xml:space="preserve"> <value>Invalid YacksSerialization attribute.</value> </data>

src\Compilers\CSharp\Portable\Declarations\SingleTypeDeclaration.cs

This source file can be found in Visual Studio Solution Explorer under CSharpCodeAnalysis - Declarations. The class declaration was changed to include the partial keyword, and two lines were added to the constructor.

internal SingleTypeDeclaration( DeclarationKind kind, string name, int arity, DeclarationModifiers modifiers, TypeDeclarationFlags declFlags, SyntaxReference syntaxReference, SourceLocation nameLocation, ICollection<string> memberNames, ImmutableArray<SingleTypeDeclaration> children, ImmutableArray<Diagnostic> diagnostics, FTypeFlagsForYacks yacksFlags = FTypeFlagsForYacks.fNone) // Added for Yacks step 06 : base(name, syntaxReference, nameLocation, diagnostics) { Debug.Assert(kind != DeclarationKind.Namespace); _kind = kind; _arity = (ushort)arity; _modifiers = modifiers; _memberNames = memberNames; _children = children; _flags = declFlags; _yacksFlags = yacksFlags; // Added for Yacks step 06 }

src\Compilers\CSharp\Portable\Declarations\SingleTypeDeclaration.Yacks.cs

This is a new file that I added to the Roslyn project.

// Copyright (c) Merlinia A/S. All Rights Reserved. Licensed under the Apache License, Version 2.0. (Just to be compatible with the Microsoft Roslyn license.) using System; namespace Microsoft.CodeAnalysis.CSharp { /// <summary> /// This source file contains some code that has been added to the SingleTypeDeclaration class. /// But to keep the inline added code to a minimum the added code is placed in methods in this /// source file and called from the main SingleTypeDeclaration.cs source file. /// </summary> partial class SingleTypeDeclaration { /// <summary> /// Flags-style enum representing the specifications provided via a possible /// [YacksSerialization()] attribute on the type declaration. /// </summary> [Flags] internal enum FTypeFlagsForYacks : byte { fNone = 0, fSerializationNone = 1 << 1, // Not relevant for types fSerializationBinary = 1 << 2, fSerializationSoap = 1 << 3, // Limited support fSerializationXml = 1 << 4 } // See enum definition above private readonly FTypeFlagsForYacks _yacksFlags; } }

src\Compilers\Core\Portable\PEWriter\MetadataWriter.cs

This source file is part of the CodeAnalysis project in Roslyn. In the Visual Studio Solution Explorer it can be found under CodeAnalysis - PEWriter.

Compared with the previous step, a few more lines of code have been added.

private void AddCustomAttributeToTable(EntityHandle parentHandle, ICustomAttribute customAttribute) { //Yacks06: Omit emitting the [YacksSerialization()] attribute (it's a "fake attribute") if (OmitYacksSerializationAttributes(customAttribute)) return; //Yacks04: Omit emitting the Code Analysis attributes if building an "anonymized" module if (OmitCodeAnalysisAttributes(customAttribute)) return; metadata.AddCustomAttribute( parent: parentHandle, constructor: GetCustomAttributeTypeCodedIndex(customAttribute.Constructor(Context)), value: GetCustomAttributeSignatureIndex(customAttribute)); }

src\Compilers\Core\Portable\PEWriter\MetadataWriter.Yacks.cs

This is a source file which has been added to the CodeAnalysis project. One additional method is added.

/// <summary> /// Method to test for a [YacksSerialization()] attribute, and if so to indicate that it can /// be omitted from the output module. This is done because the [YacksSerialization()] /// attribute is considered to be a "fake attribute", only used by the modified Roslyn /// compiler. /// </summary> private static bool OmitYacksSerializationAttributes(ICustomAttribute customAttribute) { string attributeName = GetAttributeName(customAttribute); if (attributeName == null) return false; return attributeName == Yacks_CompilationData.CYacksSerializationAttribute; }

Conclusion

As mentioned in the introduction, this modification of Roslyn doesn't add any real functionality. The new [YacksSerialization()] attribute is recognized and its usage recorded, and then it is deleted, or at least it isn't emitted to the output module. But the intention is that a later step in my efforts to modify Roslyn will use this attribute to control some built-in serialization / deserialization support that I envision.

You must login to post a comment.
Loading comment... The comment will be refreshed after 00:00.

Be the first to comment.