diff --git a/build.proj b/build.proj index b8ac7d44ed..c7eaa524dd 100644 --- a/build.proj +++ b/build.proj @@ -461,6 +461,35 @@ + + + + + "$(DotnetPath)dotnet" build "$(SqlClientNotSupportedProjectPath)" + -v:minimal + -p:Configuration=$(Configuration) + -p:NotSupportedSG=true + $(SigningKeyPathArgument) + + + $(BuildNumberArgument) + $(BuildSuffixArgument) + $(PackageVersionSqlClientArgument) + + + $(ReferenceTypeArgument) + $(PackageVersionAbstractionsArgument) + $(PackageVersionLoggingArgument) + + + $([System.Text.RegularExpressions.Regex]::Replace($(DotnetCommand), "\s+", " ")) + + + + + + PackLogging;PackAbstractions;PackSqlServer diff --git a/src/Microsoft.Data.SqlClient.slnx b/src/Microsoft.Data.SqlClient.slnx index d65c3edc90..b5991b09f1 100644 --- a/src/Microsoft.Data.SqlClient.slnx +++ b/src/Microsoft.Data.SqlClient.slnx @@ -160,6 +160,7 @@ + diff --git a/src/Microsoft.Data.SqlClient/notsupported/Microsoft.Data.SqlClient.csproj b/src/Microsoft.Data.SqlClient/notsupported/Microsoft.Data.SqlClient.csproj index a9474613e6..86c80fdffc 100644 --- a/src/Microsoft.Data.SqlClient/notsupported/Microsoft.Data.SqlClient.csproj +++ b/src/Microsoft.Data.SqlClient/notsupported/Microsoft.Data.SqlClient.csproj @@ -1,4 +1,4 @@ - + $(NoWarn);CS0618 + + $(DefineConstants);NOTSUPPORTED @@ -94,6 +96,7 @@ nice to have a flatter directory structure, it's more hassle than its worth. --> $(RepoRoot)artifacts/$(AssemblyName).notsupported/$(ReferenceType)-$(Configuration)/ + $(RepoRoot)artifacts/$(AssemblyName).notsupported/$(ReferenceType)-$(Configuration)-SG/ @@ -185,10 +188,20 @@ - $(AssemblyName).$(TargetFramework).notsupported.cs + $(AssemblyName).$(TargetFramework).notsupported.cs + Microsoft.Data.SqlClient.SourceGenerator/Microsoft.Data.SqlClient.SourceGenerator.RefToNotSupportedGenerator/$(AssemblyName).$(TargetFramework).notsupported.g.cs + + + + + + diff --git a/src/Microsoft.Data.SqlClient/ref/Microsoft.Data.SqlClient.csproj b/src/Microsoft.Data.SqlClient/ref/Microsoft.Data.SqlClient.csproj index 43aca3f1c1..78ec702632 100644 --- a/src/Microsoft.Data.SqlClient/ref/Microsoft.Data.SqlClient.csproj +++ b/src/Microsoft.Data.SqlClient/ref/Microsoft.Data.SqlClient.csproj @@ -1,4 +1,4 @@ - + Microsoft.Data.SqlClient @@ -8,6 +8,8 @@ true + true + ../notsupported @@ -21,6 +23,10 @@ $(SqlClientPackageVersion) + + + + @@ -91,6 +97,10 @@ + + false + Analyzer + diff --git a/tools/Directory.Packages.props b/tools/Directory.Packages.props index e74445ea81..e0b23d25cb 100644 --- a/tools/Directory.Packages.props +++ b/tools/Directory.Packages.props @@ -1,18 +1,19 @@ - - true - - - - - - - - - - - - - - + + true + + + + + + + + + + + + + + + diff --git a/tools/GenAPI/Microsoft.Data.SqlClient.SourceGenerator/Microsoft.Data.SqlClient.SourceGenerator.csproj b/tools/GenAPI/Microsoft.Data.SqlClient.SourceGenerator/Microsoft.Data.SqlClient.SourceGenerator.csproj new file mode 100644 index 0000000000..efab1b29a4 --- /dev/null +++ b/tools/GenAPI/Microsoft.Data.SqlClient.SourceGenerator/Microsoft.Data.SqlClient.SourceGenerator.csproj @@ -0,0 +1,18 @@ + + + + netstandard2.0 + true + false + true + + + + $(DefineConstants);GENAPI_COMPAT + + + + + + + diff --git a/tools/GenAPI/Microsoft.Data.SqlClient.SourceGenerator/Properties/launchSettings.json b/tools/GenAPI/Microsoft.Data.SqlClient.SourceGenerator/Properties/launchSettings.json new file mode 100644 index 0000000000..2292898349 --- /dev/null +++ b/tools/GenAPI/Microsoft.Data.SqlClient.SourceGenerator/Properties/launchSettings.json @@ -0,0 +1,8 @@ +{ + "profiles": { + "Debug SG": { + "commandName": "DebugRoslynComponent", + "targetProject": "..\\..\\..\\src\\Microsoft.Data.SqlClient\\ref\\Microsoft.Data.SqlClient.csproj" + } + } +} diff --git a/tools/GenAPI/Microsoft.Data.SqlClient.SourceGenerator/RefToNotSupportedGenerator.cs b/tools/GenAPI/Microsoft.Data.SqlClient.SourceGenerator/RefToNotSupportedGenerator.cs new file mode 100644 index 0000000000..7912738b2d --- /dev/null +++ b/tools/GenAPI/Microsoft.Data.SqlClient.SourceGenerator/RefToNotSupportedGenerator.cs @@ -0,0 +1,105 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace Microsoft.Data.SqlClient.SourceGenerator +{ + [Generator] + public class RefToNotSupportedGenerator : IIncrementalGenerator + { + private const string DefaultFileHeader = + "//------------------------------------------------------------------------------\r\n" + + "// \r\n" + + "// This code was generated by a tool.\r\n" + + "// {0}\r\n" + + "//\r\n" + + "// Changes to this file may cause incorrect behavior and will be lost if\r\n" + + "// the code is regenerated.\r\n" + + "// \r\n" + + "//------------------------------------------------------------------------------\r\n"; + + public void Initialize(IncrementalGeneratorInitializationContext context) + { + IncrementalValuesProvider modelsProvider = context.SyntaxProvider + .CreateSyntaxProvider( + predicate: (s, _) => IsSyntaxTargetForGeneration(s), + transform: (ctx, _) => ctx.Node) + .Where(m => m != null); // Filter out errors that we don't care about + + var modelProvider = modelsProvider + .Collect() + .Combine(context.AnalyzerConfigOptionsProvider); + + context.RegisterSourceOutput( + modelProvider, + (ctx, compilation) => + { + StringBuilder sb = new StringBuilder(); + sb.AppendFormat(DefaultFileHeader, nameof(RefToNotSupportedGenerator)); + using (StringWriter writer = new StringWriter(sb)) + { + if (!compilation.Right.GlobalOptions.TryGetValue("build_property.targetframework", out string framework)) + { + throw new KeyNotFoundException("build_property.targetframework"); + } + + try + { + sb.AppendLine("using System;"); + sb.AppendLine("#if NOTSUPPORTED"); + + RefToNotsupportedTypeRewriter visitor = new RefToNotsupportedTypeRewriter(); + // Group all collected types by namespace. Expects source file name from ref library to match namespace of all incuded types + foreach (var group in compilation.Left + .GroupBy(c => c.GetLocation().SourceTree.FilePath) + .OrderBy(cg => cg.Key) + ) + { + + sb.Append("namespace "); + sb.AppendLine(Path.GetFileNameWithoutExtension(group.Key)); + sb.AppendLine("{"); + foreach (SyntaxNode model in group) + { + SyntaxNode result = visitor.Visit(model); + result.WriteTo(writer); + } + sb.AppendLine("}"); + } + + sb.AppendLine("#endif"); + + } + catch (Exception ex) + { + sb.AppendLine($"// Exception: {ex.Message}\r\n// StackTrace: {ex.StackTrace}"); + } + + ctx.AddSource($"Microsoft.Data.SqlClient.{framework}.notsupported.g.cs", sb.ToString()); + } + }); + } + + private static bool IsSyntaxTargetForGeneration(SyntaxNode node) + { + // Public delegates + if (node is DelegateDeclarationSyntax membDecl && membDecl.Modifiers.Any(SyntaxKind.PublicKeyword)) + { + return true; + } + // Public, or internal types + if (node is BaseTypeDeclarationSyntax typeDecl && + (typeDecl.Modifiers.Any(SyntaxKind.PublicKeyword) || typeDecl.Modifiers.Any(SyntaxKind.InternalKeyword))) + { + return true; + } + + return false; + } + } +} diff --git a/tools/GenAPI/Microsoft.Data.SqlClient.SourceGenerator/RefToNotsupportedTypeRewriter.cs b/tools/GenAPI/Microsoft.Data.SqlClient.SourceGenerator/RefToNotsupportedTypeRewriter.cs new file mode 100644 index 0000000000..63e74e044d --- /dev/null +++ b/tools/GenAPI/Microsoft.Data.SqlClient.SourceGenerator/RefToNotsupportedTypeRewriter.cs @@ -0,0 +1,207 @@ +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; + +namespace Microsoft.Data.SqlClient.SourceGenerator +{ + public class RefToNotsupportedTypeRewriter : CSharpSyntaxRewriter + { + private static readonly BlockSyntax s_throwBlock; + private static readonly ArrowExpressionClauseSyntax s_throwExpression; + + static RefToNotsupportedTypeRewriter() + { + ThrowStatementSyntax throwStatement = + ThrowStatement( // throw new System.PlatformNotSupportedException("Microsoft.Data.SqlClient is not supported on this platform.") + ObjectCreationExpression( // new System.PlatformNotSupportedException("Microsoft.Data.SqlClient is not supported on this platform.") + IdentifierName("System.PlatformNotSupportedException"), + ArgumentList( // ("Microsoft.Data.SqlClient is not supported on this platform.") + SingletonSeparatedList( + Argument( // "Microsoft.Data.SqlClient is not supported on this platform." + LiteralExpression( + SyntaxKind.StringLiteralExpression, + Literal("Microsoft.Data.SqlClient is not supported on this platform.") // Microsoft.Data.SqlClient is not supported on this platform. + ) + ) + ) + ), + null + ) + ) + .WithSemicolonToken(Token(SyntaxKind.SemicolonToken)); // throw new System.PlatformNotSupportedException("Microsoft.Data.SqlClient is not supported on this platform."); + + s_throwBlock = Block(throwStatement) + .NormalizeWhitespace(indentation: " ", eol: ""); // { throw new System.PlatformNotSupportedException("Microsoft.Data.SqlClient is not supported on this platform."); } + s_throwExpression = ArrowExpressionClause(ThrowExpression(throwStatement.Expression)) + .NormalizeWhitespace(); // => throw new System.PlatformNotSupportedException("Microsoft.Data.SqlClient is not supported on this platform.") + } + + public override SyntaxNode VisitAccessorList(AccessorListSyntax node) + { + if (node.Parent.IsKind(SyntaxKind.EventDeclaration)) + { + return node; + } + + AccessorListSyntax newNode = AccessorList(); + foreach (AccessorDeclarationSyntax accessor in node.Accessors) + { + newNode = newNode.AddAccessors(AccessorDeclaration(accessor.Kind(), accessor.AttributeLists, accessor.Modifiers, s_throwBlock)); + } + + return newNode.NormalizeWhitespace(indentation: " ", eol: "").WithTrailingTrivia(CarriageReturnLineFeed); + } + + public override SyntaxNode VisitPropertyDeclaration(PropertyDeclarationSyntax node) + { + if (node.ExpressionBody != null) + { +#if GENAPI_COMPAT + // Replacing property getter expression with accessor list with throw statement for compatibility with GenAPI generated source + node = PropertyDeclaration(node.AttributeLists, node.Modifiers, node.Type, node.ExplicitInterfaceSpecifier, node.Identifier, + AccessorList(SingletonList(AccessorDeclaration(SyntaxKind.GetAccessorDeclaration, s_throwBlock))), null, null, MissingToken(SyntaxKind.SemicolonToken)) + .WithTriviaFrom(node); +#else + node = node.WithExpressionBody(ThrowExpression); +#endif + } + + return base.VisitPropertyDeclaration(node); + } + + public override SyntaxNode VisitIndexerDeclaration(IndexerDeclarationSyntax node) + { + if (node.ExpressionBody != null) + { + node = node.WithExpressionBody(s_throwExpression); + } + + return base.VisitIndexerDeclaration(node); + } + + public override SyntaxNode VisitMethodDeclaration(MethodDeclarationSyntax node) + { + if (node.Modifiers.Any(m => m.ValueText == "abstract")) + { + return base.VisitMethodDeclaration(node); + } + +#if GENAPI_COMPAT + MethodDeclarationSyntax newNode = node.WithBody(s_throwBlock) + .WithExpressionBody(null) + .WithSemicolonToken(MissingToken(SyntaxKind.SemicolonToken)) + .WithTrailingTrivia(CarriageReturnLineFeed); +#else + MethodDeclarationSyntax newNode = (node.ExpressionBody != null ? node.WithExpressionBody(s_throwExpression) : node.WithBody(s_throwBlock)) + .WithTrailingTrivia(CarriageReturnLineFeed); +#endif + return base.VisitMethodDeclaration(newNode); + } + + public override SyntaxNode VisitConstructorDeclaration(ConstructorDeclarationSyntax node) + { + ConstructorDeclarationSyntax newNode = (node.ExpressionBody != null ? node.WithExpressionBody(s_throwExpression) : node.WithBody(s_throwBlock)) + .WithTrailingTrivia(CarriageReturnLineFeed); + return base.VisitConstructorDeclaration(newNode); + } + +#if GENAPI_COMPAT + public override SyntaxNode VisitAttribute(AttributeSyntax node) + { + // Ref sources uses following form of the attribute: [System.ComponentModel.DesignerSerializationVisibilityAttribute(0)] + // , while GenAPI sources it is generated as: [System.ComponentModel.DesignerSerializationVisibilityAttribute(System.ComponentModel.DesignerSerializationVisibility.Hidden)] + // Here for GenAPI compatibility we substitute former with a latter in source + if (node.Name.ToString().EndsWith("DesignerSerializationVisibilityAttribute") && + node.ArgumentList.Arguments.Count > 0 && node.ArgumentList.ToFullString() == "(0)") + { + node = node.WithArgumentList( + AttributeArgumentList( + SingletonSeparatedList( + AttributeArgument( + MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + IdentifierName("System.ComponentModel.DesignerSerializationVisibility"), + IdentifierName("Hidden")))))); + } + + return base.VisitAttribute(node); + } +#endif + +#if GENAPI_COMPAT + // Order enum elements by value (as GenAPI does), instead of by name (as source generator does) + public override SyntaxNode VisitEnumDeclaration(EnumDeclarationSyntax node) + { + node = node.WithMembers(SeparatedList( + node.Members.OrderBy(m => int.Parse(m.EqualsValue.Value.ToFullString())).ToArray(), + node.Members.GetSeparators())); + return base.VisitEnumDeclaration(node); + } +#endif + +#if GENAPI_COMPAT + public override SyntaxNode Visit(SyntaxNode node) + { + // Strip away any documentation, or preprocessor directives + if (node is MemberDeclarationSyntax member) + { + if (!member.Modifiers.Any(m => m.IsKind(SyntaxKind.PrivateKeyword))) + { + if (member.Modifiers != null && member.Modifiers.Count > 0 && member.Modifiers[0].HasLeadingTrivia) + { + node = member.WithModifiers(member.Modifiers.Replace(member.Modifiers[0], member.Modifiers[0].WithLeadingTrivia(RemoveExcessTrivia(member.Modifiers[0].LeadingTrivia)))); + } + if (node is MethodDeclarationSyntax method && method.HasLeadingTrivia) + { + node = method.WithReturnType(method.ReturnType.WithLeadingTrivia(RemoveExcessTrivia(method.ReturnType.GetLeadingTrivia()))); + } + if (node is BasePropertyDeclarationSyntax property && property.HasLeadingTrivia) + { + node = property.WithType(property.Type.WithLeadingTrivia(RemoveExcessTrivia(property.Type.GetLeadingTrivia()))); + } + if (node is BaseTypeDeclarationSyntax typeNode) + { + node = typeNode.WithOpenBraceToken(typeNode.OpenBraceToken.WithLeadingTrivia(RemoveExcessTrivia(typeNode.OpenBraceToken.LeadingTrivia))) + .WithCloseBraceToken(typeNode.CloseBraceToken.WithLeadingTrivia(RemoveExcessTrivia(typeNode.CloseBraceToken.LeadingTrivia))); + } + } + + return base.Visit(node.WithLeadingTrivia(RemoveExcessTrivia(node.GetLeadingTrivia()))); + } + + return base.Visit(node); + } + + private static SyntaxTriviaList RemoveExcessTrivia(SyntaxTriviaList trivias) + { + SyntaxTriviaList newTrivias = TriviaList(); + for (int i = 0; i < trivias.Count; i++) + { + SyntaxTrivia trivia = trivias[i]; + SyntaxTrivia? lastTrivia = newTrivias.LastOrDefault(); + if (!trivia.IsKind(SyntaxKind.SingleLineDocumentationCommentTrivia) && + !trivia.IsKind(SyntaxKind.DisabledTextTrivia) && + !trivia.IsKind(SyntaxKind.EndIfDirectiveTrivia) && + !trivia.IsKind(SyntaxKind.IfDirectiveTrivia) && + !trivia.IsKind(SyntaxKind.ElseDirectiveTrivia) && + !trivia.IsKind(SyntaxKind.ElifDirectiveTrivia)) + { + newTrivias = newTrivias.Add(trivia); + } + else if (lastTrivia != null && + ( + lastTrivia.Value.IsKind(SyntaxKind.WhitespaceTrivia) || + lastTrivia.Value.IsKind(SyntaxKind.DisabledTextTrivia) + )) + { + newTrivias = newTrivias.Remove(lastTrivia.Value); + } + } + + return newTrivias; + } +#endif + } +} diff --git a/tools/targets/CompareMdsRefAssemblies.targets b/tools/targets/CompareMdsRefAssemblies.targets index 47caaa7a56..90651636c0 100644 --- a/tools/targets/CompareMdsRefAssemblies.targets +++ b/tools/targets/CompareMdsRefAssemblies.targets @@ -147,6 +147,51 @@ + + + + + + + + $([MSBuild]::NormalizePath('$(RepoRoot)', 'src', 'Microsoft.Data.SqlClient', 'notsupported', 'Microsoft.Data.SqlClient.csproj')) + + + + + + + + + + $(BaselinePackageDir)results\ + + + + + + + + + @@ -159,4 +204,16 @@ + + + + + + <_ApiCompatResultFiles Include="$(ApiCompatResultsDir)*.txt" /> + + + + + +