Enforce nullability for user code.

- Expanded the `ProjectWorkspaceStateGenerator` to extract the C# language version when building the `ProjectWorkspaceState`. This approach enables all platforms to get nullability support without any changes (as long as they support `ProjectWorkspaceState`, which they do). Also, Roslyn suggested that we avoid dealing with LangVersion directly because there are several factors that impact its "effective" value on a project when run in tooling.
- Updated the `LinePragma` code generation to include `#nullable restore` and `#nullable disable` lines to allow for project restored nullability state for user code.
- Added a new `RazorProjectEngineBuilderExtensions` class that adds Roslyn specific project engine modifications. In this case it allows us to set the C# language version for a project engine and configure underlying features accordingly.
- Added a `SuppressNullabilityEnforcement` flag that only turns on if C# < 8 is specified.
- Updated LiveShare, VS4Mac and RazorGenerate to understand CSharpLanguageVersion.
- Added a single test output to show the change.

dotnet/aspnetcore-tooling#5092
\n\nCommit migrated from 1df8128b87
This commit is contained in:
N. Taylor Mullen 2019-03-19 11:24:56 -07:00
parent 31e916cdfd
commit f33e1fca53
19 changed files with 195 additions and 43 deletions

View File

@ -26,7 +26,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X
if (node.Source.HasValue)
{
using (context.CodeWriter.BuildLinePragma(node.Source.Value))
using (context.CodeWriter.BuildLinePragma(node.Source.Value, context))
{
context.CodeWriter
.WriteLine(RazorInjectAttribute)

View File

@ -26,7 +26,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X
if (node.Source.HasValue)
{
using (context.CodeWriter.BuildLinePragma(node.Source.Value))
using (context.CodeWriter.BuildLinePragma(node.Source.Value, context))
{
context.CodeWriter
.WriteLine(RazorInjectAttribute)

View File

@ -26,7 +26,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions
if (node.Source.HasValue)
{
using (context.CodeWriter.BuildLinePragma(node.Source.Value))
using (context.CodeWriter.BuildLinePragma(node.Source.Value, context))
{
context.CodeWriter
.WriteLine(RazorInjectAttribute)

View File

@ -440,7 +440,7 @@ namespace Microsoft.AspNetCore.Razor.Language.CodeGeneration
return new CSharpCodeWritingScope(writer);
}
public static IDisposable BuildLinePragma(this CodeWriter writer, SourceSpan? span)
public static IDisposable BuildLinePragma(this CodeWriter writer, SourceSpan? span, CodeRenderingContext context)
{
if (string.IsNullOrEmpty(span?.FilePath))
{
@ -448,7 +448,7 @@ namespace Microsoft.AspNetCore.Razor.Language.CodeGeneration
return NullDisposable.Default;
}
return new LinePragmaWriter(writer, span.Value);
return new LinePragmaWriter(writer, span.Value, context.Options);
}
private static void WriteVerbatimStringLiteral(CodeWriter writer, string literal)
@ -596,9 +596,13 @@ namespace Microsoft.AspNetCore.Razor.Language.CodeGeneration
private class LinePragmaWriter : IDisposable
{
private readonly CodeWriter _writer;
private readonly RazorCodeGenerationOptions _codeGenerationOptions;
private readonly int _startIndent;
public LinePragmaWriter(CodeWriter writer, SourceSpan span)
public LinePragmaWriter(
CodeWriter writer,
SourceSpan span,
RazorCodeGenerationOptions codeGenerationOptions)
{
if (writer == null)
{
@ -606,8 +610,15 @@ namespace Microsoft.AspNetCore.Razor.Language.CodeGeneration
}
_writer = writer;
_codeGenerationOptions = codeGenerationOptions;
_startIndent = _writer.CurrentIndent;
_writer.CurrentIndent = 0;
if (!_codeGenerationOptions.SuppressNullabilityEnforcement)
{
_writer.WriteLine("#nullable restore");
}
WriteLineNumberDirective(writer, span);
}
@ -630,6 +641,11 @@ namespace Microsoft.AspNetCore.Razor.Language.CodeGeneration
.WriteLine("#line default")
.WriteLine("#line hidden");
if (!_codeGenerationOptions.SuppressNullabilityEnforcement)
{
_writer.WriteLine("#nullable disable");
}
_writer.CurrentIndent = _startIndent;
}
}

View File

@ -13,7 +13,7 @@ namespace Microsoft.AspNetCore.Razor.Language.CodeGeneration
{
if (node.Source.HasValue)
{
using (context.CodeWriter.BuildLinePragma(node.Source.Value))
using (context.CodeWriter.BuildLinePragma(node.Source.Value, context))
{
context.AddSourceMappingFor(node);
context.CodeWriter.WriteUsing(node.Content);
@ -44,7 +44,7 @@ namespace Microsoft.AspNetCore.Razor.Language.CodeGeneration
if (node.Source != null)
{
using (context.CodeWriter.BuildLinePragma(node.Source.Value))
using (context.CodeWriter.BuildLinePragma(node.Source.Value, context))
{
var offset = DesignTimeDirectivePass.DesignTimeVariable.Length + " = ".Length;
context.CodeWriter.WritePadding(offset, node.Source, context);
@ -91,7 +91,7 @@ namespace Microsoft.AspNetCore.Razor.Language.CodeGeneration
IDisposable linePragmaScope = null;
if (node.Source != null)
{
linePragmaScope = context.CodeWriter.BuildLinePragma(node.Source.Value);
linePragmaScope = context.CodeWriter.BuildLinePragma(node.Source.Value, context);
context.CodeWriter.WritePadding(0, node.Source.Value, context);
}
@ -150,7 +150,7 @@ namespace Microsoft.AspNetCore.Razor.Language.CodeGeneration
var firstChild = node.Children[0];
if (firstChild.Source != null)
{
using (context.CodeWriter.BuildLinePragma(firstChild.Source.Value))
using (context.CodeWriter.BuildLinePragma(firstChild.Source.Value, context))
{
var offset = DesignTimeDirectivePass.DesignTimeVariable.Length + " = ".Length;
context.CodeWriter.WritePadding(offset, firstChild.Source, context);
@ -210,7 +210,7 @@ namespace Microsoft.AspNetCore.Razor.Language.CodeGeneration
{
if (!isWhitespaceStatement)
{
linePragmaScope = context.CodeWriter.BuildLinePragma(token.Source.Value);
linePragmaScope = context.CodeWriter.BuildLinePragma(token.Source.Value, context);
}
context.CodeWriter.WritePadding(0, token.Source.Value, context);

View File

@ -32,7 +32,7 @@ namespace Microsoft.AspNetCore.Razor.Language.CodeGeneration
{
if (node.Source.HasValue)
{
using (context.CodeWriter.BuildLinePragma(node.Source.Value))
using (context.CodeWriter.BuildLinePragma(node.Source.Value, context))
{
context.CodeWriter.WriteUsing(node.Content);
}
@ -58,7 +58,7 @@ namespace Microsoft.AspNetCore.Razor.Language.CodeGeneration
IDisposable linePragmaScope = null;
if (node.Source != null)
{
linePragmaScope = context.CodeWriter.BuildLinePragma(node.Source.Value);
linePragmaScope = context.CodeWriter.BuildLinePragma(node.Source.Value, context);
context.CodeWriter.WritePadding(WriteCSharpExpressionMethod.Length + 1, node.Source, context);
}
@ -103,7 +103,7 @@ namespace Microsoft.AspNetCore.Razor.Language.CodeGeneration
IDisposable linePragmaScope = null;
if (node.Source != null)
{
linePragmaScope = context.CodeWriter.BuildLinePragma(node.Source.Value);
linePragmaScope = context.CodeWriter.BuildLinePragma(node.Source.Value, context);
context.CodeWriter.WritePadding(0, node.Source.Value, context);
}
@ -200,7 +200,7 @@ namespace Microsoft.AspNetCore.Razor.Language.CodeGeneration
public override void WriteCSharpExpressionAttributeValue(CodeRenderingContext context, CSharpExpressionAttributeValueIntermediateNode node)
{
using (context.CodeWriter.BuildLinePragma(node.Source.Value))
using (context.CodeWriter.BuildLinePragma(node.Source.Value, context))
{
var prefixLocation = node.Source.Value.AbsoluteIndex;
context.CodeWriter
@ -266,7 +266,7 @@ namespace Microsoft.AspNetCore.Razor.Language.CodeGeneration
{
if (!isWhitespaceStatement)
{
linePragmaScope = context.CodeWriter.BuildLinePragma(token.Source.Value);
linePragmaScope = context.CodeWriter.BuildLinePragma(token.Source.Value, context);
}
context.CodeWriter.WritePadding(0, token.Source.Value, context);

View File

@ -61,7 +61,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Components
if (node.Source.HasValue)
{
using (context.CodeWriter.BuildLinePragma(node.Source.Value))
using (context.CodeWriter.BuildLinePragma(node.Source.Value, context))
{
context.AddSourceMappingFor(node);
context.CodeWriter.WriteUsing(node.Content);
@ -92,7 +92,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Components
if (node.Source != null)
{
using (context.CodeWriter.BuildLinePragma(node.Source.Value))
using (context.CodeWriter.BuildLinePragma(node.Source.Value, context))
{
var offset = DesignTimeVariable.Length + " = ".Length;
context.CodeWriter.WritePadding(offset, node.Source, context);
@ -162,7 +162,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Components
{
if (!isWhitespaceStatement)
{
linePragmaScope = context.CodeWriter.BuildLinePragma(node.Source.Value);
linePragmaScope = context.CodeWriter.BuildLinePragma(node.Source.Value, context);
}
context.CodeWriter.WritePadding(0, node.Source.Value, context);
@ -247,7 +247,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Components
var firstChild = node.Children[0];
if (firstChild.Source != null)
{
using (context.CodeWriter.BuildLinePragma(firstChild.Source.Value))
using (context.CodeWriter.BuildLinePragma(firstChild.Source.Value, context))
{
var offset = DesignTimeVariable.Length + " = ".Length;
context.CodeWriter.WritePadding(offset, firstChild.Source, context);
@ -835,7 +835,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Components
return;
}
using (context.CodeWriter.BuildLinePragma(token.Source))
using (context.CodeWriter.BuildLinePragma(token.Source, context))
{
context.CodeWriter.WritePadding(0, token.Source.Value, context);
context.AddSourceMappingFor(token);

View File

@ -55,7 +55,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Components
IDisposable linePragmaScope = null;
if (node.Source != null)
{
linePragmaScope = context.CodeWriter.BuildLinePragma(node.Source.Value);
linePragmaScope = context.CodeWriter.BuildLinePragma(node.Source.Value, context);
context.CodeWriter.WritePadding(0, node.Source.Value, context);
}

View File

@ -12,7 +12,8 @@ namespace Microsoft.AspNetCore.Razor.Language
string rootNamespace,
bool suppressChecksum,
bool supressMetadataAttributes,
bool suppressPrimaryMethodBody)
bool suppressPrimaryMethodBody,
bool suppressNullabilityEnforcement)
{
IndentWithTabs = indentWithTabs;
IndentSize = indentSize;
@ -21,6 +22,7 @@ namespace Microsoft.AspNetCore.Razor.Language
SuppressChecksum = suppressChecksum;
SuppressMetadataAttributes = supressMetadataAttributes;
SuppressPrimaryMethodBody = suppressPrimaryMethodBody;
SuppressNullabilityEnforcement = suppressNullabilityEnforcement;
}
public override bool DesignTime { get; }
@ -32,5 +34,7 @@ namespace Microsoft.AspNetCore.Razor.Language
public override string RootNamespace { get; }
public override bool SuppressChecksum { get; }
public override bool SuppressNullabilityEnforcement { get; }
}
}

View File

@ -37,6 +37,8 @@ namespace Microsoft.AspNetCore.Razor.Language
public override bool SuppressChecksum { get; set; }
public override bool SuppressNullabilityEnforcement { get; set; }
public override RazorCodeGenerationOptions Build()
{
return new DefaultRazorCodeGenerationOptions(
@ -46,7 +48,8 @@ namespace Microsoft.AspNetCore.Razor.Language
RootNamespace,
SuppressChecksum,
SuppressMetadataAttributes,
SuppressPrimaryMethodBody);
SuppressPrimaryMethodBody,
SuppressNullabilityEnforcement);
}
public override void SetDesignTime(bool designTime)

View File

@ -373,7 +373,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Extensions
var firstMappedChild = node.Children.FirstOrDefault(child => child.Source != null) as IntermediateNode;
var valueStart = firstMappedChild?.Source;
using (context.CodeWriter.BuildLinePragma(node.Source))
using (context.CodeWriter.BuildLinePragma(node.Source, context))
{
var accessor = GetPropertyAccessor(node);
var assignmentPrefixLength = accessor.Length + " = ".Length;
@ -422,7 +422,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Extensions
}
else
{
using (context.CodeWriter.BuildLinePragma(node.Source))
using (context.CodeWriter.BuildLinePragma(node.Source, context))
{
context.CodeWriter.WriteStartAssignment(GetPropertyAccessor(node));

View File

@ -65,7 +65,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Extensions
}
// {node.Content} __typeHelper = default({node.Content});
using (context.CodeWriter.BuildLinePragma(node.Source))
using (context.CodeWriter.BuildLinePragma(node.Source, context))
{
context.AddSourceMappingFor(node);
context.CodeWriter
@ -88,7 +88,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Extensions
}
// global::System.Object {node.content} = null;
using (context.CodeWriter.BuildLinePragma(node.Source))
using (context.CodeWriter.BuildLinePragma(node.Source, context))
{
context.CodeWriter
.Write("global::")
@ -112,7 +112,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Extensions
}
// global::System.Object __typeHelper = nameof({node.Content});
using (context.CodeWriter.BuildLinePragma(node.Source))
using (context.CodeWriter.BuildLinePragma(node.Source, context))
{
context.CodeWriter
.Write("global::")
@ -132,7 +132,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Extensions
case DirectiveTokenKind.String:
// global::System.Object __typeHelper = "{node.Content}";
using (context.CodeWriter.BuildLinePragma(node.Source))
using (context.CodeWriter.BuildLinePragma(node.Source, context))
{
context.CodeWriter
.Write("global::")
@ -173,7 +173,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Extensions
// for consistency so when a C# completion session starts, filling user code doesn't result in
// a previously non-existent line pragma from being added and destroying the context in which
// the completion session was started.
using (context.CodeWriter.BuildLinePragma(node.Source))
using (context.CodeWriter.BuildLinePragma(node.Source, context))
{
context.AddSourceMappingFor(node);
context.CodeWriter.Write(" ");

View File

@ -16,7 +16,8 @@ namespace Microsoft.AspNetCore.Razor.Language
suppressChecksum: false,
rootNamespace: null,
supressMetadataAttributes: false,
suppressPrimaryMethodBody: false);
suppressPrimaryMethodBody: false,
suppressNullabilityEnforcement: false);
}
public static RazorCodeGenerationOptions CreateDesignTimeDefault()
@ -28,7 +29,8 @@ namespace Microsoft.AspNetCore.Razor.Language
rootNamespace: null,
suppressChecksum: false,
supressMetadataAttributes: true,
suppressPrimaryMethodBody: false);
suppressPrimaryMethodBody: false,
suppressNullabilityEnforcement: false);
}
public static RazorCodeGenerationOptions Create(Action<RazorCodeGenerationOptionsBuilder> configure)
@ -107,5 +109,10 @@ namespace Microsoft.AspNetCore.Razor.Language
/// Gets or sets a value that determines if an empty body is generated for the primary method.
/// </summary>
public virtual bool SuppressPrimaryMethodBody { get; protected set; }
/// <summary>
/// Gets or sets a value that determines if nullability type enforcement is restored to project settings for user code.
/// </summary>
public virtual bool SuppressNullabilityEnforcement { get; }
}
}

View File

@ -54,6 +54,11 @@ namespace Microsoft.AspNetCore.Razor.Language
/// </summary>
public virtual bool SuppressPrimaryMethodBody { get; set; }
/// <summary>
/// Gets or sets a value that determines if nullability type enforcement is restored to project settings for user code.
/// </summary>
public virtual bool SuppressNullabilityEnforcement { get; set; }
public abstract RazorCodeGenerationOptions Build();
public virtual void SetDesignTime(bool designTime)

View File

@ -7,6 +7,7 @@ using System.Diagnostics;
using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Razor;
using Microsoft.Extensions.CommandLineUtils;
using Microsoft.VisualStudio.LanguageServices.Razor.Serialization;
@ -30,6 +31,7 @@ namespace Microsoft.AspNetCore.Razor.Tools
ExtensionNames = Option("-n", "extension name", CommandOptionType.MultipleValue);
ExtensionFilePaths = Option("-e", "extension file path", CommandOptionType.MultipleValue);
RootNamespace = Option("--root-namespace", "root namespace for generated code", CommandOptionType.SingleValue);
CSharpLanguageVersion = Option("--csharp-language-version", "csharp language version generated code", CommandOptionType.SingleValue);
GenerateDeclaration = Option("--generate-declaration", "Generate declaration", CommandOptionType.NoValue);
}
@ -55,6 +57,8 @@ namespace Microsoft.AspNetCore.Razor.Tools
public CommandOption RootNamespace { get; }
public CommandOption CSharpLanguageVersion { get; }
public CommandOption GenerateDeclaration { get; }
protected override Task<int> ExecuteCoreAsync()
@ -170,6 +174,22 @@ namespace Microsoft.AspNetCore.Razor.Tools
RazorProjectFileSystem.Create(projectDirectory),
});
var success = true;
var csharpLanguageVersion = LanguageVersion.Default;
if (CSharpLanguageVersion.HasValue())
{
var rawLanguageVersion = CSharpLanguageVersion.Value();
if (!LanguageVersionFacts.TryParse(CSharpLanguageVersion.Value(), out var parsedLanguageVersion))
{
success = false;
Error.WriteLine($"Unknown C# language version {rawLanguageVersion}.");
}
else
{
csharpLanguageVersion = parsedLanguageVersion;
}
}
var engine = RazorProjectEngine.Create(configuration, compositeFileSystem, b =>
{
b.Features.Add(new StaticTagHelperFeature() { TagHelpers = tagHelpers, });
@ -184,12 +204,12 @@ namespace Microsoft.AspNetCore.Razor.Tools
{
b.SetRootNamespace(RootNamespace.Value());
}
b.SetCSharpLanguageVersion(csharpLanguageVersion);
});
var results = GenerateCode(engine, sourceItems);
var success = true;
foreach (var result in results)
{
var errorCount = result.CSharpDocument.Diagnostics.Count;

View File

@ -0,0 +1,85 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Linq;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.CodeAnalysis.CSharp;
namespace Microsoft.CodeAnalysis.Razor
{
/// <summary>
/// Roslyn specific <see cref="RazorProjectEngineBuilder"/> extensions.
/// </summary>
public static class RazorProjectEngineBuilderExtensions
{
/// <summary>
/// Sets the C# language version to respect when generating code.
/// </summary>
/// <param name="builder">The <see cref="RazorProjectEngineBuilder"/>.</param>
/// <param name="csharpLanguageVersion">The C# <see cref="LanguageVersion"/>.</param>
/// <returns>The <see cref="RazorProjectEngineBuilder"/>.</returns>
public static RazorProjectEngineBuilder SetCSharpLanguageVersion(this RazorProjectEngineBuilder builder, LanguageVersion csharpLanguageVersion)
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
}
if (builder.Configuration.LanguageVersion.Major < 3)
{
// Prior to 3.0 there were no C# version specific controlled features so there's no value in setting a CSharp language version, noop.
return builder;
}
var existingFeature = builder.Features.OfType<ConfigureParserForCSharpVersionFeature>().FirstOrDefault();
if (existingFeature != null)
{
builder.Features.Remove(existingFeature);
}
builder.Features.Add(new ConfigureParserForCSharpVersionFeature(csharpLanguageVersion));
return builder;
}
private class ConfigureParserForCSharpVersionFeature : IConfigureRazorCodeGenerationOptionsFeature
{
public ConfigureParserForCSharpVersionFeature(LanguageVersion csharpLanguageVersion)
{
CSharpLanguageVersion = csharpLanguageVersion;
}
public LanguageVersion CSharpLanguageVersion { get; }
public int Order { get; set; }
public RazorEngine Engine { get; set; }
public void Configure(RazorCodeGenerationOptionsBuilder options)
{
if (options == null)
{
throw new ArgumentNullException(nameof(options));
}
if (CSharpLanguageVersion < LanguageVersion.CSharp8)
{
options.SuppressNullabilityEnforcement = true;
}
else
{
// Given that nullability enforcement can be a compile error we only turn it on for C# >= 8.0. There are
// cases in tooling when the project isn't fully configured yet at which point the CSharpLanguageVersion
// may be Default (value 0). In those cases that C# version is equivalently "unspecified" and is up to the consumer
// to act in a safe manner to not cause unneeded errors for older compilers. Therefore if the version isn't
// >= 8.0 (or Latest) then nullability enforcement is suppressed.
//
// Once the project finishes configuration the C# language version will be updated to reflect the effective
// language version for the project.
options.SuppressNullabilityEnforcement = false;
}
}
}
}
}

View File

@ -26,6 +26,8 @@ namespace Microsoft.AspNetCore.Razor.Tasks
public string RootNamespace { get; set; }
public string CSharpLanguageVersion { get; set; }
[Required]
public string Version { get; set; }
@ -138,16 +140,24 @@ namespace Microsoft.AspNetCore.Razor.Tasks
builder.AppendLine(Configuration[0].GetMetadata(Identity));
// Added in 3.0
if (parsedVersion.Major >= 3 && !string.IsNullOrEmpty(RootNamespace))
if (parsedVersion.Major >= 3)
{
builder.AppendLine("--root-namespace");
builder.AppendLine(RootNamespace);
}
if (!string.IsNullOrEmpty(RootNamespace))
{
builder.AppendLine("--root-namespace");
builder.AppendLine(RootNamespace);
}
// Added in 3.0
if (parsedVersion.Major >= 3 && GenerateDeclaration)
{
builder.AppendLine("--generate-declaration");
if (!string.IsNullOrEmpty(CSharpLanguageVersion))
{
builder.AppendLine("--csharp-language-version");
builder.AppendLine(CSharpLanguageVersion);
}
if (GenerateDeclaration)
{
builder.AppendLine("--generate-declaration");
}
}
for (var i = 0; i < Extensions.Length; i++)

View File

@ -156,6 +156,7 @@ Copyright (c) .NET Foundation. All rights reserved.
PipeName="$(_RazorBuildServerPipeName)"
Version="$(RazorLangVersion)"
RootNamespace="$(RootNamespace)"
CSharpLanguageVersion="$(LangVersion)"
Configuration="@(ResolvedRazorConfiguration)"
Extensions="@(ResolvedRazorExtension)"
Sources="@(RazorGenerateWithTargetPath)"

View File

@ -107,6 +107,7 @@ Copyright (c) .NET Foundation. All rights reserved.
PipeName="$(_RazorBuildServerPipeName)"
Version="$(RazorLangVersion)"
RootNamespace="$(RootNamespace)"
CSharpLanguageVersion="$(LangVersion)"
Configuration="@(ResolvedRazorConfiguration)"
Extensions="@(ResolvedRazorExtension)"
Sources="@(_RazorComponentDeclarationSources)"