8000 Remove nullable disable warnings by jaredpar · Pull Request #48846 · dotnet/sdk · GitHub
[go: up one dir, main page]

Skip to content

Conversation

@jaredpar
Copy link
Member
@jaredpar jaredpar commented May 7, 2025

This change removes all uses of #nullable disable warnings in the code base. This means the files now get full nullable validation where previously they were using annotations with no compiler enforcement. For the most part this change just follows the warnings that result from removing the directive but there are a few small checks that got added, like Debug.Assert, that are worth looking at.

I will also call out in the review a few patterns in the sdk / System.CommandLine that are hard to work with in nullable.

Note: to make this easier to review I have added review comments any place I made a change that was not purely mechanical in nature.

Copilot AI review requested due to automatic review settings May 7, 2025 19:46
Copy link
Contributor
Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This pull request removes bulk usages of "#nullable disable warnings" to enforce full nullable validation across the codebase. In doing so, it updates type annotations and parameters in multiple files to adapt to stricter nullability rules.

  • Removed "#nullable disable warnings" directives from various files.
  • Adjusted method and record parameter types to non-nullable where appropriate.
  • Added a null-coalescing fallback to ensure string values are not unexpectedly null.

Reviewed Changes

Copilot reviewed 44 out of 44 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
src/Cli/dotnet/Commands/Sdk/Check/SdkOutputWriter.cs Removed "#nullable disable warnings".
src/Cli/dotnet/Commands/Sdk/Check/RuntimeOutputWriter.cs Removed "#nullable disable warnings".
src/Cli/dotnet/Commands/Run/RunProperties.cs Updated RunProperties record to enforce a non-null RunCommand.
src/Cli/dotnet/Commands/Run/RunCommand.cs Changed restoreArgs parameter to be non-nullable.
src/Cli/dotnet/Commands/Run/LaunchSettings/* Removed "#nullable disable warnings".
src/Cli/dotnet/Commands/Run/CommonRunHelpers.cs Adjusted parameters from nullable to non-nullable for consistency.
src/Cli/dotnet/Commands/New/PostActions/* Removed "#nullable disable warnings".
src/Cli/dotnet/Commands/New/NewCommandParser.cs Added a fallback for NetStandardImplicitPackageVersion.
src/Cli/dotnet/Commands/New/MSBuildEvaluation/* Removed "#nullable disable warnings" and updated dictionary types.
src/Cli/dotnet/Commands/New/DotnetCommandCallbacks.cs Removed "#nullable disable warnings".
src/Cli/dotnet/CommandFactory/CommandSpec.cs Updated parameter types and default values for clarity.
Comments suppressed due to low confidence (5)

src/Cli/dotnet/CommandFactory/CommandSpec.cs:8

  • Updating the 8000 path parameter to be non-nullable assumes that all initializations of CommandSpec provide a valid path. Verify that this contract is maintained across the codebase to prevent unexpected null values.
public class CommandSpec( string path, string? args, Dictionary<string, string>? environmentVariables = null)

src/Cli/dotnet/Commands/Run/RunCommand.cs:324

  • Converting the restoreArgs parameter from nullable to non-nullable requires confirming that all call sites pass a valid array. Verify that this change aligns with caller expectations and handle potential null values upstream if needed.
static ProjectInstance EvaluateProject(string? projectFilePath, Func<ProjectCollection, ProjectInstance>? projectFactory, string[] restoreArgs, ILogger? binaryLogger)

src/Cli/dotnet/Commands/Run/CommonRunHelpers.cs:37

  • Updating the args parameter to be non-nullable enforces a stricter contract. Ensure that all callers are updated to provide a non-null string array.
public static Dictionary<string, string> GetGlobalPropertiesFromArgs(string[] args)

src/Cli/dotnet/Commands/New/NewCommandParser.cs:171

  • [nitpick] Using an empty string as a fallback for NetStandardImplicitPackageVersion is acceptable if it conforms with downstream expectations. Confirm that this fallback value appropriately handles cases where the version string might be null.
{ "NetStandardImplicitPackageVersion", new FrameworkDependencyFile().GetNetStandardLibraryVersion() ?? "" },

src/Cli/dotnet/Commands/New/MSBuildEvaluation/MSBuildEvaluator.cs:198

  • Allowing null values in the properties dictionary may lead to downstream issues if consumers assume non-null strings. Review the consumers of this dictionary to ensure proper null handling or consider defaulting to empty strings where appropriate.
Dictionary<string, string?> properties = new()

public string Path { get; } = path;

public string Args { get; } = args;
public string? Args { get; } = args;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having Args be potentially null seems suspicious, I considered changing this to the following but don't know enough about the code base to say if that is okay

public string Args { get; } = args ?? "";

{ "dotnet-cli-version", Product.Version },
{ "RuntimeFrameworkVersion", new Muxer().SharedFxVersion },
{ "NetStandardImplicitPackageVersion", new FrameworkDependencyFile().GetNetStandardLibraryVersion() },
{ "NetStandardImplicitPackageVersion", new FrameworkDependencyFile().GetNetStandardLibraryVersion() ?? "" },
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@baronfel should verify this change as being accurate. It is also possible the templating engine is wrong and it should accept a Dictionary<string, string?> but I don't have the depth to say which is better.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The GetNetStandardLibraryVersion method shouldn't return a nullable - it's one of that hard-coded, required things and we should blow up if it's not present.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Happy to change that to throw on failure if you think it's the best course.

namespace Microsoft.DotNet.Cli.Commands.Run;

internal record RunProperties(string? RunCommand, string? RunArguments, string? RunWorkingDirectory)
internal record RunProperties(string RunCommand, string? RunArguments, string? RunWorkingDirectory)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is another place where having arguments, specifically RunArguments, be potentially null feels potentially off and "" may work.

public SolutionAddCommand(ParseResult parseResult) : base(parseResult)
{
_fileOrDirectory = parseResult.GetValue(SolutionCommandParser.SlnArgument);
_fileOrDirectory = parseResult.GetValue(SolutionCommandParser.SlnArgument)!;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ParseResult type in general does not work well with nullable analysis because GetValue is hard coded to return T?. One possible solution is add a GetValueOrThrow extension method. Happy to make that change if it's desired.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We chatted about this briefly but the main gap here is that the S.CL library enforces the arity of an option/argument but there's no way for that Arity to influence the return type of this method.

Comment on lines 196 to +197
var projectPlatform = projectInstancePlatforms.FirstOrDefault(
platform => platform.Replace(" ", string.Empty) == solutionPlatform.Replace(" ", string.Empty), projectInstancePlatforms.FirstOrDefault());
platform => platform.Replace(" ", string.Empty) == solutionPlatform.Replace(" ", string.Empty), projectInstancePlatforms.FirstOrDefault()!);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lacking the ! on the default value (last argument) the compiler will end up inferring FirstOrDefault<string?> not FirstOrDefault<string>. That is because the default value directly infers the value for T and FirstOrDefault is null returning. Don't know enough about the semantics here to suggest a more null friendly pattern.

internal sealed record FileArtifactMessage(string? FullPath, string? DisplayName, string? Description, string? TestUid, string? TestDisplayName, string? SessionUid);

internal sealed record FileArtifactMessages(string? ExecutionId, string InstanceId, FileArtifactMessage[] FileArtifacts) : IRequest;
internal sealed record FileArtifactMessages(string? ExecutionId, string? InstanceId, FileArtifactMessage[] FileArtifacts) : IRequest;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems like most record types in the sdk default to putting ? on all parameters. I suspect that many could, and should, be unannotated.

internal sealed record Handshake(Dictionary<byte, string>? Properties);

internal sealed record CommandLineOption(string? Name, string? Description, bool? IsHidden, bool? IsBuiltIn);
internal sealed record CommandLineOption(string Name, string Description, bool? IsHidden, bool? IsBuiltIn);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment about record declarations

{
AggregateException aggregate => aggregate.Flatten().InnerExceptions,
_ => [exception?.InnerException],
_ => [exception?.InnerException!],
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is known issue in collection expression null inference that I needed to work around. @cston

Description = CliCommandStrings.CmdTestCaseFilterDescription,
HelpName = CliCommandStrings.CmdTestCaseFilterExpression
}.ForwardAsSingle(o => $"-property:VSTestTestCaseFilter={SurroundWithDoubleQuotes(o)}");
}.ForwardAsSingle(o => $"-property:VSTestTestCaseFilter={SurroundWithDoubleQuotes(o!)}");
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Most of the ! is related to the ParseResult issue mentioned above.

@jaredpar jaredpar changed the title Remove nullable disable warnings part 1 Remove nullable disable warnings May 9, 2025
{
var projectBuildType = projectInstanceBuildTypes.FirstOrDefault(
buildType => buildType.Replace(" ", string.Empty) == solutionBuildType.Replace(" ", string.Empty), projectInstanceBuildTypes.FirstOrDefault());
buildType => buildType.Replace(" ", string.Empty) == solutionBuildType.Replace(" ", string.Empty), projectInstanceBuildTypes.FirstOrDefault()!);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same FirstOrDefault pattern as above.

Comment on lines +113 to +130
if (nugetPackageDownloader is not null)
{
IsPackageDownloaderProvided = true;
PackageDownloader = nugetPackageDownloader;
}
else
{
IsPackageDownloaderProvided = false;
PackageDownloader = new NuGetPackageDownloader.NuGetPackageDownloader(
TempPackagesDirectory,
filePermissionSetter: null,
new FirstPartyNuGetPackageSigningVerifier(),
nugetLogger,
Reporter,
restoreActionConfig: RestoreActionConfiguration,
verifySignatures: VerifySignatures,
shouldUsePackageSourceMapping: true);
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is just a simple expansion of the original code that is more NRT friendly. Could also change this to be a tuple return but this seemed more readable.

? null
: Environment.GetEnvironmentVariable(key));
var ridFileName = "NETCoreSdkRuntimeIdentifierChain.txt";
var sdkPath = dotnetRootPath is not null ? Path.Combine(dotnetRootPath, "sdk") : "sdk";
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a case where the NRT analysis seems to have spotted a legitimate bug in the code. In the case that dotnetRootpath is null then the Path.Combine calls below will throw. I changed this to just use "sdk" as a relative path but possible that a throws here is a better idea.

// The .NET Foundation licenses this file to you under the MIT license.

#nullable disable warnings
#nullable disable
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For most of the build tasks there was a single ? annotation. Instead of fully annotating the build tasks, which is hundreds of changes, I just removed the errant annotation and disable NRT in the file. Seems much better to approach those as a separate change where a good deal of thought is put into how NRT should be structured in tasks.

@jaredpar
Copy link
Member Author
jaredpar commented May 9, 2025

@marcpopMSFT, @MiYanni, @baronfel

This is a follow up change where I removed all uses of #nullable disable warnings from the sdk. This puts the SDK into a pretty clean state with respect to nullable annotations. To make this easier to review I put review comments in GitHub anywhere that I made a non-mechanical change to the code.

{
HelpName = RuntimeArgName
}.ForwardAsMany(RuntimeArgFunc)
}.ForwardAsMany(RuntimeArgFunc!)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why did you need the ! for these? The static function is known, isn't it?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

public static Option<T> ForwardAsMany<T>(this ForwardedOption<T> option, Func<T?, IEnumerable<string>> format);

This requires that the delegate have a string? parameter, presumably more fallout of ParseResult design. The RuntimeArgFunc though depends on having a non-null parameter. The parameter is immediately passed to GetArchFromRid and that throws on null.

@jaredpar jaredpar merged commit 4a7b8ec into main May 12, 2025
30 checks passed
@jaredpar jaredpar deleted the dev/jaredpar/nrt1 branch May 12, 2025 21:23
@Forgind
Copy link
Contributor
Forgind commented May 13, 2025

I believe that if you do not have a dotnet.config in your current directory (or a parent/grandparent/etc. directory), this change means that you cannot use this version of the SDK with debug bits because of a new Debug.Assert.

@Forgind
Copy link
Contributor
Forgind commented May 13, 2025

Process terminated.
Assertion failed.
dotnetConfigPath is not null
at Microsoft.DotNet.Cli.Commands.Test.TestCommandParser.GetTestRunnerName() in C:\GitHub\sdkMain\src\Cli\dotnet\Commands\Test\TestCommandParser.cs:line 167
at Microsoft.DotNet.Cli.Commands.Test.TestCommandParser.ConstructCommand() in C:\GitHub\sdkMain\src\Cli\dotnet\Commands\Test\TestCommandParser.cs:line 212
at Microsoft.DotNet.Cli.Commands.Test.TestCommandParser..cctor() in C:\GitHub\sdkMain\src\Cli\dotnet\Commands\Test\TestCommandParser.cs:line 155
at Microsoft.DotNet.Cli.Commands.Test.TestCommandParser.GetCommand() in C:\GitHub\sdkMain\src\Cli\dotnet\Commands\Test\TestCommandParser.cs:line 159
at Microsoft.DotNet.Cli.Parser..cctor() in C:\GitHub\sdkMain\src\Cli\dotnet\Parser.cs:line 62
at Microsoft.DotNet.Cli.Parser.get_Instance() in C:\GitHub\sdkMain\src\Cli\dotnet\Parser.cs:line 214
at Microsoft.DotNet.Cli.Program.ProcessArgs(String[] args, TimeSpan startupTime, ITelemetry telemetryClient) in C:\GitHub\sdkMain\src\Cli\dotnet\Program.cs:line 128
at Microsoft.DotNet.Cli.Program.Main(String[] args) in C:\GitHub\sdkMain\src\Cli\dotnet\Program.cs:line 77

@Forgind Forgind mentioned this pull request May 13, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these 4408 issues.

5 participants

0