Merge pull request #87137 from /dotnet/nullable-godot-tools

This commit is contained in:
Rémi Verschelde 2024-02-17 18:31:37 +01:00
commit ccd9cefa8e
No known key found for this signature in database
GPG key ID: C3336907360768E1
47 changed files with 349 additions and 194 deletions

View file

@ -7,11 +7,11 @@ namespace GodotTools.BuildLogger
{ {
public class GodotBuildLogger : ILogger public class GodotBuildLogger : ILogger
{ {
public string Parameters { get; set; } public string? Parameters { get; set; }
public LoggerVerbosity Verbosity { get; set; } public LoggerVerbosity Verbosity { get; set; }
private StreamWriter _logStreamWriter; private StreamWriter _logStreamWriter = StreamWriter.Null;
private StreamWriter _issuesStreamWriter; private StreamWriter _issuesStreamWriter = StreamWriter.Null;
private int _indent; private int _indent;
public void Initialize(IEventSource eventSource) public void Initialize(IEventSource eventSource)

View file

@ -3,6 +3,7 @@
<ProjectGuid>{6CE9A984-37B1-4F8A-8FE9-609F05F071B3}</ProjectGuid> <ProjectGuid>{6CE9A984-37B1-4F8A-8FE9-609F05F071B3}</ProjectGuid>
<TargetFramework>net6.0</TargetFramework> <TargetFramework>net6.0</TargetFramework>
<LangVersion>10</LangVersion> <LangVersion>10</LangVersion>
<Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.Build.Framework" Version="15.1.548" ExcludeAssets="runtime" /> <PackageReference Include="Microsoft.Build.Framework" Version="15.1.548" ExcludeAssets="runtime" />

View file

@ -3,5 +3,6 @@
<ProjectGuid>{639E48BD-44E5-4091-8EDD-22D36DC0768D}</ProjectGuid> <ProjectGuid>{639E48BD-44E5-4091-8EDD-22D36DC0768D}</ProjectGuid>
<TargetFramework>net6.0</TargetFramework> <TargetFramework>net6.0</TargetFramework>
<LangVersion>10</LangVersion> <LangVersion>10</LangVersion>
<Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>
</Project> </Project>

View file

@ -11,7 +11,7 @@ namespace GodotTools.Core
{ {
var tcs = new TaskCompletionSource<bool>(); var tcs = new TaskCompletionSource<bool>();
void ProcessExited(object sender, EventArgs e) void ProcessExited(object? sender, EventArgs e)
{ {
tcs.TrySetResult(true); tcs.TrySetResult(true);
} }

View file

@ -7,7 +7,7 @@ namespace GodotTools.Core
{ {
public static class StringExtensions public static class StringExtensions
{ {
private static readonly string _driveRoot = Path.GetPathRoot(Environment.CurrentDirectory); private static readonly string _driveRoot = Path.GetPathRoot(Environment.CurrentDirectory)!;
public static string RelativeToPath(this string path, string dir) public static string RelativeToPath(this string path, string dir)
{ {
@ -26,9 +26,6 @@ namespace GodotTools.Core
public static string NormalizePath(this string path) public static string NormalizePath(this string path)
{ {
if (string.IsNullOrEmpty(path))
return path;
bool rooted = path.IsAbsolutePath(); bool rooted = path.IsAbsolutePath();
path = path.Replace('\\', '/'); path = path.Replace('\\', '/');

View file

@ -4,6 +4,7 @@
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework> <TargetFramework>net6.0</TargetFramework>
<LangVersion>10</LangVersion> <LangVersion>10</LangVersion>
<Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\GodotTools.IdeMessaging\GodotTools.IdeMessaging.csproj" /> <ProjectReference Include="..\GodotTools.IdeMessaging\GodotTools.IdeMessaging.csproj" />

View file

@ -64,14 +64,14 @@ namespace GodotTools.IdeMessaging.CLI
while (!fwdClient.IsDisposed) while (!fwdClient.IsDisposed)
{ {
string firstLine = await inputReader.ReadLineAsync(); string? firstLine = await inputReader.ReadLineAsync();
if (firstLine == null || firstLine == "QUIT") if (firstLine == null || firstLine == "QUIT")
goto ExitMainLoop; goto ExitMainLoop;
string messageId = firstLine; string messageId = firstLine;
string messageArgcLine = await inputReader.ReadLineAsync(); string? messageArgcLine = await inputReader.ReadLineAsync();
if (messageArgcLine == null) if (messageArgcLine == null)
{ {
@ -89,7 +89,7 @@ namespace GodotTools.IdeMessaging.CLI
for (int i = 0; i < messageArgc; i++) for (int i = 0; i < messageArgc; i++)
{ {
string bodyLine = await inputReader.ReadLineAsync(); string? bodyLine = await inputReader.ReadLineAsync();
if (bodyLine == null) if (bodyLine == null)
{ {
@ -126,29 +126,29 @@ namespace GodotTools.IdeMessaging.CLI
} }
} }
private static async Task<Response> SendRequest(Client client, string id, MessageContent content) private static async Task<Response?> SendRequest(Client client, string id, MessageContent content)
{ {
var handlers = new Dictionary<string, Func<Task<Response>>> var handlers = new Dictionary<string, Func<Task<Response?>>>
{ {
[PlayRequest.Id] = async () => [PlayRequest.Id] = async () =>
{ {
var request = JsonConvert.DeserializeObject<PlayRequest>(content.Body); var request = JsonConvert.DeserializeObject<PlayRequest>(content.Body);
return await client.SendRequest<PlayResponse>(request); return await client.SendRequest<PlayResponse>(request!);
}, },
[DebugPlayRequest.Id] = async () => [DebugPlayRequest.Id] = async () =>
{ {
var request = JsonConvert.DeserializeObject<DebugPlayRequest>(content.Body); var request = JsonConvert.DeserializeObject<DebugPlayRequest>(content.Body);
return await client.SendRequest<DebugPlayResponse>(request); return await client.SendRequest<DebugPlayResponse>(request!);
}, },
[ReloadScriptsRequest.Id] = async () => [ReloadScriptsRequest.Id] = async () =>
{ {
var request = JsonConvert.DeserializeObject<ReloadScriptsRequest>(content.Body); var request = JsonConvert.DeserializeObject<ReloadScriptsRequest>(content.Body);
return await client.SendRequest<ReloadScriptsResponse>(request); return await client.SendRequest<ReloadScriptsResponse>(request!);
}, },
[CodeCompletionRequest.Id] = async () => [CodeCompletionRequest.Id] = async () =>
{ {
var request = JsonConvert.DeserializeObject<CodeCompletionRequest>(content.Body); var request = JsonConvert.DeserializeObject<CodeCompletionRequest>(content.Body);
return await client.SendRequest<CodeCompletionResponse>(request); return await client.SendRequest<CodeCompletionResponse>(request!);
} }
}; };

View file

@ -1,4 +1,5 @@
using System; using System;
using System.Diagnostics.CodeAnalysis;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Net; using System.Net;
@ -27,7 +28,7 @@ namespace GodotTools.IdeMessaging
private readonly IMessageHandler messageHandler; private readonly IMessageHandler messageHandler;
private Peer peer; private Peer? peer;
private readonly SemaphoreSlim connectionSem = new SemaphoreSlim(1); private readonly SemaphoreSlim connectionSem = new SemaphoreSlim(1);
private readonly Queue<NotifyAwaiter<bool>> clientConnectedAwaiters = new Queue<NotifyAwaiter<bool>>(); private readonly Queue<NotifyAwaiter<bool>> clientConnectedAwaiters = new Queue<NotifyAwaiter<bool>>();
@ -53,6 +54,7 @@ namespace GodotTools.IdeMessaging
public bool IsDisposed { get; private set; } public bool IsDisposed { get; private set; }
// ReSharper disable once MemberCanBePrivate.Global // ReSharper disable once MemberCanBePrivate.Global
[MemberNotNullWhen(true, "peer")]
public bool IsConnected => peer != null && !peer.IsDisposed && peer.IsTcpClientConnected; public bool IsConnected => peer != null && !peer.IsDisposed && peer.IsTcpClientConnected;
// ReSharper disable once EventNeverSubscribedTo.Global // ReSharper disable once EventNeverSubscribedTo.Global
@ -111,7 +113,7 @@ namespace GodotTools.IdeMessaging
if (disposing) if (disposing)
{ {
peer?.Dispose(); peer?.Dispose();
fsWatcher?.Dispose(); fsWatcher.Dispose();
} }
} }
@ -215,12 +217,12 @@ namespace GodotTools.IdeMessaging
using (var fileStream = new FileStream(MetaFilePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) using (var fileStream = new FileStream(MetaFilePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
using (var reader = new StreamReader(fileStream)) using (var reader = new StreamReader(fileStream))
{ {
string portStr = reader.ReadLine(); string? portStr = reader.ReadLine();
if (portStr == null) if (portStr == null)
return null; return null;
string editorExecutablePath = reader.ReadLine(); string? editorExecutablePath = reader.ReadLine();
if (editorExecutablePath == null) if (editorExecutablePath == null)
return null; return null;
@ -333,7 +335,7 @@ namespace GodotTools.IdeMessaging
} }
} }
public async Task<TResponse> SendRequest<TResponse>(Request request) public async Task<TResponse?> SendRequest<TResponse>(Request request)
where TResponse : Response, new() where TResponse : Response, new()
{ {
if (!IsConnected) if (!IsConnected)
@ -346,7 +348,7 @@ namespace GodotTools.IdeMessaging
return await peer.SendRequest<TResponse>(request.Id, body); return await peer.SendRequest<TResponse>(request.Id, body);
} }
public async Task<TResponse> SendRequest<TResponse>(string id, string body) public async Task<TResponse?> SendRequest<TResponse>(string id, string body)
where TResponse : Response, new() where TResponse : Response, new()
{ {
if (!IsConnected) if (!IsConnected)

View file

@ -1,3 +1,4 @@
using System.Diagnostics.CodeAnalysis;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
namespace GodotTools.IdeMessaging namespace GodotTools.IdeMessaging
@ -9,7 +10,7 @@ namespace GodotTools.IdeMessaging
public string GetHandshakeLine(string identity) => $"{ClientHandshakeBase},{identity}"; public string GetHandshakeLine(string identity) => $"{ClientHandshakeBase},{identity}";
public bool IsValidPeerHandshake(string handshake, out string identity, ILogger logger) public bool IsValidPeerHandshake(string handshake, [NotNullWhen(true)] out string? identity, ILogger logger)
{ {
identity = null; identity = null;

View file

@ -42,7 +42,7 @@ namespace GodotTools.IdeMessaging
[OpenFileRequest.Id] = async (peer, content) => [OpenFileRequest.Id] = async (peer, content) =>
{ {
var request = JsonConvert.DeserializeObject<OpenFileRequest>(content.Body); var request = JsonConvert.DeserializeObject<OpenFileRequest>(content.Body);
return await HandleOpenFile(request); return await HandleOpenFile(request!);
} }
}; };
} }

View file

@ -0,0 +1,142 @@
// ReSharper disable once CheckNamespace
namespace System.Diagnostics.CodeAnalysis
{
/// <summary>Specifies that null is allowed as an input even if the corresponding type disallows it.</summary>
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property, Inherited = false)]
internal sealed class AllowNullAttribute : Attribute
{ }
/// <summary>Specifies that null is disallowed as an input even if the corresponding type allows it.</summary>
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property, Inherited = false)]
internal sealed class DisallowNullAttribute : Attribute
{ }
/// <summary>Specifies that an output may be null even if the corresponding type disallows it.</summary>
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, Inherited = false)]
internal sealed class MaybeNullAttribute : Attribute
{ }
/// <summary>Specifies that an output will not be null even if the corresponding type allows it. Specifies that an input argument was not null when the call returns.</summary>
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, Inherited = false)]
internal sealed class NotNullAttribute : Attribute
{ }
/// <summary>Specifies that when a method returns <see cref="ReturnValue"/>, the parameter may be null even if the corresponding type disallows it.</summary>
[AttributeUsage(AttributeTargets.Parameter, Inherited = false)]
internal sealed class MaybeNullWhenAttribute : Attribute
{
/// <summary>Initializes the attribute with the specified return value condition.</summary>
/// <param name="returnValue">
/// The return value condition. If the method returns this value, the associated parameter may be null.
/// </param>
public MaybeNullWhenAttribute(bool returnValue) => ReturnValue = returnValue;
/// <summary>Gets the return value condition.</summary>
public bool ReturnValue { get; }
}
/// <summary>Specifies that when a method returns <see cref="ReturnValue"/>, the parameter will not be null even if the corresponding type allows it.</summary>
[AttributeUsage(AttributeTargets.Parameter, Inherited = false)]
internal sealed class NotNullWhenAttribute : Attribute
{
/// <summary>Initializes the attribute with the specified return value condition.</summary>
/// <param name="returnValue">
/// The return value condition. If the method returns this value, the associated parameter will not be null.
/// </param>
public NotNullWhenAttribute(bool returnValue) => ReturnValue = returnValue;
/// <summary>Gets the return value condition.</summary>
public bool ReturnValue { get; }
}
/// <summary>Specifies that the output will be non-null if the named parameter is non-null.</summary>
[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, AllowMultiple = true, Inherited = false)]
internal sealed class NotNullIfNotNullAttribute : Attribute
{
/// <summary>Initializes the attribute with the associated parameter name.</summary>
/// <param name="parameterName">
/// The associated parameter name. The output will be non-null if the argument to the parameter specified is non-null.
/// </param>
public NotNullIfNotNullAttribute(string parameterName) => ParameterName = parameterName;
/// <summary>Gets the associated parameter name.</summary>
public string ParameterName { get; }
}
/// <summary>Applied to a method that will never return under any circumstance.</summary>
[AttributeUsage(AttributeTargets.Method, Inherited = false)]
internal sealed class DoesNotReturnAttribute : Attribute
{ }
/// <summary>Specifies that the method will not return if the associated Boolean parameter is passed the specified value.</summary>
[AttributeUsage(AttributeTargets.Parameter, Inherited = false)]
internal sealed class DoesNotReturnIfAttribute : Attribute
{
/// <summary>Initializes the attribute with the specified parameter value.</summary>
/// <param name="parameterValue">
/// The condition parameter value. Code after the method will be considered unreachable by diagnostics if the argument to
/// the associated parameter matches this value.
/// </param>
public DoesNotReturnIfAttribute(bool parameterValue) => ParameterValue = parameterValue;
/// <summary>Gets the condition parameter value.</summary>
public bool ParameterValue { get; }
}
/// <summary>Specifies that the method or property will ensure that the listed field and property members have not-null values.</summary>
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = false, AllowMultiple = true)]
internal sealed class MemberNotNullAttribute : Attribute
{
/// <summary>Initializes the attribute with a field or property member.</summary>
/// <param name="member">
/// The field or property member that is promised to be not-null.
/// </param>
public MemberNotNullAttribute(string member) => Members = new[] { member };
/// <summary>Initializes the attribute with the list of field and property members.</summary>
/// <param name="members">
/// The list of field and property members that are promised to be not-null.
/// </param>
public MemberNotNullAttribute(params string[] members) => Members = members;
/// <summary>Gets field or property member names.</summary>
public string[] Members { get; }
}
/// <summary>Specifies that the method or property will ensure that the listed field and property members have not-null values when returning with the specified return value condition.</summary>
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = false, AllowMultiple = true)]
internal sealed class MemberNotNullWhenAttribute : Attribute
{
/// <summary>Initializes the attribute with the specified return value condition and a field or property member.</summary>
/// <param name="returnValue">
/// The return value condition. If the method returns this value, the associated parameter will not be null.
/// </param>
/// <param name="member">
/// The field or property member that is promised to be not-null.
/// </param>
public MemberNotNullWhenAttribute(bool returnValue, string member)
{
ReturnValue = returnValue;
Members = new[] { member };
}
/// <summary>Initializes the attribute with the specified return value condition and list of field and property members.</summary>
/// <param name="returnValue">
/// The return value condition. If the method returns this value, the associated parameter will not be null.
/// </param>
/// <param name="members">
/// The list of field and property members that are promised to be not-null.
/// </param>
public MemberNotNullWhenAttribute(bool returnValue, params string[] members)
{
ReturnValue = returnValue;
Members = members;
}
/// <summary>Gets the return value condition.</summary>
public bool ReturnValue { get; }
/// <summary>Gets field or property member names.</summary>
public string[] Members { get; }
}
}

View file

@ -1,3 +1,5 @@
using System.Diagnostics.CodeAnalysis;
namespace GodotTools.IdeMessaging namespace GodotTools.IdeMessaging
{ {
public readonly struct GodotIdeMetadata public readonly struct GodotIdeMetadata
@ -23,7 +25,7 @@ namespace GodotTools.IdeMessaging
return !(a == b); return !(a == b);
} }
public override bool Equals(object obj) public override bool Equals([NotNullWhen(true)] object? obj)
{ {
return obj is GodotIdeMetadata metadata && metadata == this; return obj is GodotIdeMetadata metadata && metadata == this;
} }

View file

@ -2,9 +2,10 @@
<PropertyGroup> <PropertyGroup>
<ProjectGuid>{92600954-25F0-4291-8E11-1FEE9FC4BE20}</ProjectGuid> <ProjectGuid>{92600954-25F0-4291-8E11-1FEE9FC4BE20}</ProjectGuid>
<TargetFramework>netstandard2.0</TargetFramework> <TargetFramework>netstandard2.0</TargetFramework>
<LangVersion>7.2</LangVersion> <LangVersion>9</LangVersion>
<Nullable>enable</Nullable>
<PackageId>GodotTools.IdeMessaging</PackageId> <PackageId>GodotTools.IdeMessaging</PackageId>
<Version>1.1.1</Version> <Version>1.1.2</Version>
<AssemblyVersion>$(Version)</AssemblyVersion> <AssemblyVersion>$(Version)</AssemblyVersion>
<Authors>Godot Engine contributors</Authors> <Authors>Godot Engine contributors</Authors>
<Company /> <Company />

View file

@ -1,8 +1,10 @@
using System.Diagnostics.CodeAnalysis;
namespace GodotTools.IdeMessaging namespace GodotTools.IdeMessaging
{ {
public interface IHandshake public interface IHandshake
{ {
string GetHandshakeLine(string identity); string GetHandshakeLine(string identity);
bool IsValidPeerHandshake(string handshake, out string identity, ILogger logger); bool IsValidPeerHandshake(string handshake, [NotNullWhen(true)] out string? identity, ILogger logger);
} }
} }

View file

@ -8,7 +8,7 @@ namespace GodotTools.IdeMessaging
private class DecodedMessage private class DecodedMessage
{ {
public MessageKind? Kind; public MessageKind? Kind;
public string Id; public string? Id;
public MessageStatus? Status; public MessageStatus? Status;
public readonly StringBuilder Body = new StringBuilder(); public readonly StringBuilder Body = new StringBuilder();
public uint? PendingBodyLines; public uint? PendingBodyLines;
@ -41,7 +41,7 @@ namespace GodotTools.IdeMessaging
private readonly DecodedMessage decodingMessage = new DecodedMessage(); private readonly DecodedMessage decodingMessage = new DecodedMessage();
public State Decode(string messageLine, out Message decodedMessage) public State Decode(string messageLine, out Message? decodedMessage)
{ {
decodedMessage = null; decodedMessage = null;

View file

@ -46,11 +46,11 @@ namespace GodotTools.IdeMessaging
private readonly SemaphoreSlim writeSem = new SemaphoreSlim(1); private readonly SemaphoreSlim writeSem = new SemaphoreSlim(1);
private string remoteIdentity = string.Empty; private string? remoteIdentity;
public string RemoteIdentity => remoteIdentity; public string RemoteIdentity => remoteIdentity ??= string.Empty;
public event Action Connected; public event Action? Connected;
public event Action Disconnected; public event Action? Disconnected;
private ILogger Logger { get; } private ILogger Logger { get; }
@ -87,7 +87,7 @@ namespace GodotTools.IdeMessaging
{ {
var decoder = new MessageDecoder(); var decoder = new MessageDecoder();
string messageLine; string? messageLine;
while ((messageLine = await ReadLine()) != null) while ((messageLine = await ReadLine()) != null)
{ {
var state = decoder.Decode(messageLine, out var msg); var state = decoder.Decode(messageLine, out var msg);
@ -105,7 +105,7 @@ namespace GodotTools.IdeMessaging
try try
{ {
if (msg.Kind == MessageKind.Request) if (msg!.Kind == MessageKind.Request)
{ {
var responseContent = await messageHandler.HandleRequest(this, msg.Id, msg.Content, Logger); var responseContent = await messageHandler.HandleRequest(this, msg.Id, msg.Content, Logger);
await WriteMessage(new Message(MessageKind.Response, msg.Id, responseContent)); await WriteMessage(new Message(MessageKind.Response, msg.Id, responseContent));
@ -163,9 +163,9 @@ namespace GodotTools.IdeMessaging
return false; return false;
} }
string peerHandshake = await readHandshakeTask; string? peerHandshake = await readHandshakeTask;
if (handshake == null || !handshake.IsValidPeerHandshake(peerHandshake, out remoteIdentity, Logger)) if (peerHandshake == null || !handshake.IsValidPeerHandshake(peerHandshake, out remoteIdentity, Logger))
{ {
Logger.LogError("Received invalid handshake: " + peerHandshake); Logger.LogError("Received invalid handshake: " + peerHandshake);
return false; return false;
@ -179,7 +179,7 @@ namespace GodotTools.IdeMessaging
return true; return true;
} }
private async Task<string> ReadLine() private async Task<string?> ReadLine()
{ {
try try
{ {
@ -216,7 +216,7 @@ namespace GodotTools.IdeMessaging
return WriteLine(builder.ToString()); return WriteLine(builder.ToString());
} }
public async Task<TResponse> SendRequest<TResponse>(string id, string body) public async Task<TResponse?> SendRequest<TResponse>(string id, string body)
where TResponse : Response, new() where TResponse : Response, new()
{ {
ResponseAwaiter responseAwaiter; ResponseAwaiter responseAwaiter;
@ -243,7 +243,7 @@ namespace GodotTools.IdeMessaging
private async Task<bool> WriteLine(string text) private async Task<bool> WriteLine(string text)
{ {
if (clientWriter == null || IsDisposed || !IsTcpClientConnected) if (IsDisposed || !IsTcpClientConnected)
return false; return false;
using (await writeSem.UseAsync()) using (await writeSem.UseAsync())
@ -290,9 +290,9 @@ namespace GodotTools.IdeMessaging
Disconnected?.Invoke(); Disconnected?.Invoke();
} }
clientReader?.Dispose(); clientReader.Dispose();
clientWriter?.Dispose(); clientWriter.Dispose();
((IDisposable)tcpClient)?.Dispose(); ((IDisposable)tcpClient).Dispose();
} }
} }
} }

View file

@ -2,6 +2,7 @@
// ReSharper disable UnusedMember.Global // ReSharper disable UnusedMember.Global
// ReSharper disable UnusedAutoPropertyAccessor.Global // ReSharper disable UnusedAutoPropertyAccessor.Global
using System;
using Newtonsoft.Json; using Newtonsoft.Json;
namespace GodotTools.IdeMessaging.Requests namespace GodotTools.IdeMessaging.Requests
@ -38,7 +39,7 @@ namespace GodotTools.IdeMessaging.Requests
} }
public CompletionKind Kind { get; set; } public CompletionKind Kind { get; set; }
public string ScriptFile { get; set; } public string ScriptFile { get; set; } = string.Empty;
public new const string Id = "CodeCompletion"; public new const string Id = "CodeCompletion";
@ -50,8 +51,8 @@ namespace GodotTools.IdeMessaging.Requests
public sealed class CodeCompletionResponse : Response public sealed class CodeCompletionResponse : Response
{ {
public CodeCompletionRequest.CompletionKind Kind; public CodeCompletionRequest.CompletionKind Kind;
public string ScriptFile { get; set; } public string ScriptFile { get; set; } = string.Empty;
public string[] Suggestions { get; set; } public string[] Suggestions { get; set; } = Array.Empty<string>();
} }
public sealed class PlayRequest : Request public sealed class PlayRequest : Request
@ -82,7 +83,7 @@ namespace GodotTools.IdeMessaging.Requests
public sealed class DebugPlayRequest : Request public sealed class DebugPlayRequest : Request
{ {
public string DebuggerHost { get; set; } public string DebuggerHost { get; set; } = string.Empty;
public int DebuggerPort { get; set; } public int DebuggerPort { get; set; }
public bool? BuildBeforePlaying { get; set; } public bool? BuildBeforePlaying { get; set; }
@ -99,7 +100,7 @@ namespace GodotTools.IdeMessaging.Requests
public sealed class OpenFileRequest : Request public sealed class OpenFileRequest : Request
{ {
public string File { get; set; } public string File { get; set; } = string.Empty;
public int? Line { get; set; } public int? Line { get; set; }
public int? Column { get; set; } public int? Column { get; set; }

View file

@ -15,7 +15,7 @@ namespace GodotTools.IdeMessaging
public override void SetResult(MessageContent content) public override void SetResult(MessageContent content)
{ {
if (content.Status == MessageStatus.Ok) if (content.Status == MessageStatus.Ok)
SetResult(JsonConvert.DeserializeObject<T>(content.Body)); SetResult(JsonConvert.DeserializeObject<T>(content.Body)!);
else else
SetResult(new T { Status = content.Status }); SetResult(new T { Status = content.Status });
} }

View file

@ -1,3 +1,6 @@
// ReSharper disable ParameterHidesMember
// ReSharper disable UnusedMember.Global
using System; using System;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
@ -5,9 +8,9 @@ namespace GodotTools.IdeMessaging.Utils
{ {
public class NotifyAwaiter<T> : INotifyCompletion public class NotifyAwaiter<T> : INotifyCompletion
{ {
private Action continuation; private Action? continuation;
private Exception exception; private Exception? exception;
private T result; private T? result;
public bool IsCompleted { get; private set; } public bool IsCompleted { get; private set; }
@ -15,7 +18,7 @@ namespace GodotTools.IdeMessaging.Utils
{ {
if (exception != null) if (exception != null)
throw exception; throw exception;
return result; return result!;
} }
public void OnCompleted(Action continuation) public void OnCompleted(Action continuation)

View file

@ -4,6 +4,7 @@
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<TargetFramework>net6.0-windows</TargetFramework> <TargetFramework>net6.0-windows</TargetFramework>
<LangVersion>10</LangVersion> <LangVersion>10</LangVersion>
<Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="Exists('$(SolutionDir)/../../../../bin/GodotSharp/Api/Debug/GodotSharp.dll') And ('$(GodotPlatform)' == 'windows' Or ('$(GodotPlatform)' == '' And '$(OS)' == 'Windows_NT'))"> <PropertyGroup Condition="Exists('$(SolutionDir)/../../../../bin/GodotSharp/Api/Debug/GodotSharp.dll') And ('$(GodotPlatform)' == 'windows' Or ('$(GodotPlatform)' == '' And '$(OS)' == 'Windows_NT'))">
<OutputPath>$(SolutionDir)/../../../../bin/GodotSharp/Tools</OutputPath> <OutputPath>$(SolutionDir)/../../../../bin/GodotSharp/Tools</OutputPath>

View file

@ -53,6 +53,12 @@ namespace GodotTools.OpenVisualStudio
{ {
// Launch of VS 2022 failed, fallback to 2019 // Launch of VS 2022 failed, fallback to 2019
dte = TryVisualStudioLaunch("VisualStudio.DTE.16.0"); dte = TryVisualStudioLaunch("VisualStudio.DTE.16.0");
if (dte == null)
{
Console.Error.WriteLine("Visual Studio not found");
return 1;
}
} }
dte.UserControl = true; dte.UserControl = true;
@ -137,12 +143,12 @@ namespace GodotTools.OpenVisualStudio
return 0; return 0;
} }
private static DTE TryVisualStudioLaunch(string version) private static DTE? TryVisualStudioLaunch(string version)
{ {
try try
{ {
var visualStudioDteType = Type.GetTypeFromProgID(version, throwOnError: true); var visualStudioDteType = Type.GetTypeFromProgID(version, throwOnError: true);
var dte = (DTE)Activator.CreateInstance(visualStudioDteType); var dte = (DTE?)Activator.CreateInstance(visualStudioDteType!);
return dte; return dte;
} }
@ -152,7 +158,7 @@ namespace GodotTools.OpenVisualStudio
} }
} }
private static DTE FindInstanceEditingSolution(string solutionPath) private static DTE? FindInstanceEditingSolution(string solutionPath)
{ {
if (GetRunningObjectTable(0, out IRunningObjectTable pprot) != 0) if (GetRunningObjectTable(0, out IRunningObjectTable pprot) != 0)
return null; return null;
@ -218,7 +224,7 @@ namespace GodotTools.OpenVisualStudio
// Class containing the IOleMessageFilter // Class containing the IOleMessageFilter
// thread error-handling functions // thread error-handling functions
private static IOleMessageFilter _oldFilter; private static IOleMessageFilter? _oldFilter;
// Start the filter // Start the filter
public static void Register() public static void Register()
@ -268,7 +274,7 @@ namespace GodotTools.OpenVisualStudio
// Implement the IOleMessageFilter interface // Implement the IOleMessageFilter interface
[DllImport("ole32.dll")] [DllImport("ole32.dll")]
private static extern int CoRegisterMessageFilter(IOleMessageFilter newFilter, out IOleMessageFilter oldFilter); private static extern int CoRegisterMessageFilter(IOleMessageFilter? newFilter, out IOleMessageFilter? oldFilter);
} }
[ComImport(), Guid("00000016-0000-0000-C000-000000000046"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] [ComImport(), Guid("00000016-0000-0000-C000-000000000046"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]

View file

@ -34,22 +34,23 @@ EndProject";
@" {{{0}}}.{1}|Any CPU.ActiveCfg = {1}|Any CPU @" {{{0}}}.{1}|Any CPU.ActiveCfg = {1}|Any CPU
{{{0}}}.{1}|Any CPU.Build.0 = {1}|Any CPU"; {{{0}}}.{1}|Any CPU.Build.0 = {1}|Any CPU";
private string _directoryPath;
private readonly Dictionary<string, ProjectInfo> _projects = new Dictionary<string, ProjectInfo>(); private readonly Dictionary<string, ProjectInfo> _projects = new Dictionary<string, ProjectInfo>();
public string Name { get; } public string Name { get; }
public string DirectoryPath { get; }
public string DirectoryPath
{
get => _directoryPath;
set => _directoryPath = value.IsAbsolutePath() ? value : Path.GetFullPath(value);
}
public class ProjectInfo public class ProjectInfo
{ {
public string Guid; public string Guid { get; }
public string PathRelativeToSolution; public string PathRelativeToSolution { get; }
public List<string> Configs = new List<string>(); public List<string> Configs { get; }
public ProjectInfo(string guid, string pathRelativeToSolution, List<string> configs)
{
Guid = guid;
PathRelativeToSolution = pathRelativeToSolution;
Configs = configs;
}
} }
public void AddNewProject(string name, ProjectInfo projectInfo) public void AddNewProject(string name, ProjectInfo projectInfo)
@ -117,9 +118,10 @@ EndProject";
File.WriteAllText(solutionPath, content, Encoding.UTF8); // UTF-8 with BOM File.WriteAllText(solutionPath, content, Encoding.UTF8); // UTF-8 with BOM
} }
public DotNetSolution(string name) public DotNetSolution(string name, string directoryPath)
{ {
Name = name; Name = name;
DirectoryPath = directoryPath.IsAbsolutePath() ? directoryPath : Path.GetFullPath(directoryPath);
} }
public static void MigrateFromOldConfigNames(string slnPath) public static void MigrateFromOldConfigNames(string slnPath)

View file

@ -3,6 +3,7 @@
<ProjectGuid>{A8CDAD94-C6D4-4B19-A7E7-76C53CC92984}</ProjectGuid> <ProjectGuid>{A8CDAD94-C6D4-4B19-A7E7-76C53CC92984}</ProjectGuid>
<TargetFramework>net6.0</TargetFramework> <TargetFramework>net6.0</TargetFramework>
<LangVersion>10</LangVersion> <LangVersion>10</LangVersion>
<Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.Build" Version="15.1.548" ExcludeAssets="runtime" /> <PackageReference Include="Microsoft.Build" Version="15.1.548" ExcludeAssets="runtime" />

View file

@ -34,7 +34,7 @@ namespace GodotTools.ProjectEditor
public static void MSBuildLocatorRegisterMSBuildPath(string msbuildPath) public static void MSBuildLocatorRegisterMSBuildPath(string msbuildPath)
=> MSBuildLocator.RegisterMSBuildPath(msbuildPath); => MSBuildLocator.RegisterMSBuildPath(msbuildPath);
public static MSBuildProject Open(string path) public static MSBuildProject? Open(string path)
{ {
var root = ProjectRootElement.Open(path); var root = ProjectRootElement.Open(path);
return root != null ? new MSBuildProject(root) : null; return root != null ? new MSBuildProject(root) : null;

View file

@ -1,5 +1,3 @@
#nullable enable
namespace GodotTools.Build namespace GodotTools.Build
{ {
public class BuildDiagnostic public class BuildDiagnostic

View file

@ -1,11 +1,10 @@
using System; using System;
using System.Diagnostics.CodeAnalysis;
using Godot; using Godot;
using Godot.Collections; using Godot.Collections;
using GodotTools.Internals; using GodotTools.Internals;
using Path = System.IO.Path; using Path = System.IO.Path;
#nullable enable
namespace GodotTools.Build namespace GodotTools.Build
{ {
[Serializable] [Serializable]
@ -25,7 +24,7 @@ namespace GodotTools.Build
public string LogsDirPath => GodotSharpDirs.LogsDirPathFor(Solution, Configuration); public string LogsDirPath => GodotSharpDirs.LogsDirPathFor(Solution, Configuration);
public override bool Equals(object? obj) public override bool Equals([NotNullWhen(true)] object? obj)
{ {
return obj is BuildInfo other && return obj is BuildInfo other &&
other.Solution == Solution && other.Solution == Solution &&

View file

@ -11,18 +11,18 @@ namespace GodotTools.Build
{ {
public static class BuildManager public static class BuildManager
{ {
private static BuildInfo _buildInProgress; private static BuildInfo? _buildInProgress;
public const string MsBuildIssuesFileName = "msbuild_issues.csv"; public const string MsBuildIssuesFileName = "msbuild_issues.csv";
private const string MsBuildLogFileName = "msbuild_log.txt"; private const string MsBuildLogFileName = "msbuild_log.txt";
public delegate void BuildLaunchFailedEventHandler(BuildInfo buildInfo, string reason); public delegate void BuildLaunchFailedEventHandler(BuildInfo buildInfo, string reason);
public static event BuildLaunchFailedEventHandler BuildLaunchFailed; public static event BuildLaunchFailedEventHandler? BuildLaunchFailed;
public static event Action<BuildInfo> BuildStarted; public static event Action<BuildInfo>? BuildStarted;
public static event Action<BuildResult> BuildFinished; public static event Action<BuildResult>? BuildFinished;
public static event Action<string> StdOutputReceived; public static event Action<string?>? StdOutputReceived;
public static event Action<string> StdErrorReceived; public static event Action<string?>? StdErrorReceived;
public static DateTime LastValidBuildDateTime { get; private set; } public static DateTime LastValidBuildDateTime { get; private set; }
@ -274,8 +274,8 @@ namespace GodotTools.Build
} }
private static BuildInfo CreateBuildInfo( private static BuildInfo CreateBuildInfo(
[DisallowNull] string configuration, string configuration,
[AllowNull] string platform = null, string? platform = null,
bool rebuild = false, bool rebuild = false,
bool onlyClean = false bool onlyClean = false
) )
@ -294,10 +294,10 @@ namespace GodotTools.Build
} }
private static BuildInfo CreatePublishBuildInfo( private static BuildInfo CreatePublishBuildInfo(
[DisallowNull] string configuration, string configuration,
[DisallowNull] string platform, string platform,
[DisallowNull] string runtimeIdentifier, string runtimeIdentifier,
[DisallowNull] string publishOutputDir, string publishOutputDir,
bool includeDebugSymbols = true bool includeDebugSymbols = true
) )
{ {
@ -319,20 +319,20 @@ namespace GodotTools.Build
} }
public static bool BuildProjectBlocking( public static bool BuildProjectBlocking(
[DisallowNull] string configuration, string configuration,
[AllowNull] string platform = null, string? platform = null,
bool rebuild = false bool rebuild = false
) => BuildProjectBlocking(CreateBuildInfo(configuration, platform, rebuild)); ) => BuildProjectBlocking(CreateBuildInfo(configuration, platform, rebuild));
public static bool CleanProjectBlocking( public static bool CleanProjectBlocking(
[DisallowNull] string configuration, string configuration,
[AllowNull] string platform = null string? platform = null
) => CleanProjectBlocking(CreateBuildInfo(configuration, platform, rebuild: false, onlyClean: true)); ) => CleanProjectBlocking(CreateBuildInfo(configuration, platform, rebuild: false, onlyClean: true));
public static bool PublishProjectBlocking( public static bool PublishProjectBlocking(
[DisallowNull] string configuration, string configuration,
[DisallowNull] string platform, string platform,
[DisallowNull] string runtimeIdentifier, string runtimeIdentifier,
string publishOutputDir, string publishOutputDir,
bool includeDebugSymbols = true bool includeDebugSymbols = true
) => PublishProjectBlocking(CreatePublishBuildInfo(configuration, ) => PublishProjectBlocking(CreatePublishBuildInfo(configuration,

View file

@ -1,8 +1,6 @@
using Godot; using Godot;
using static GodotTools.Internals.Globals; using static GodotTools.Internals.Globals;
#nullable enable
namespace GodotTools.Build namespace GodotTools.Build
{ {
public partial class BuildOutputView : HBoxContainer public partial class BuildOutputView : HBoxContainer

View file

@ -1,7 +1,5 @@
using Godot; using Godot;
#nullable enable
namespace GodotTools.Build namespace GodotTools.Build
{ {
public class BuildProblemsFilter public class BuildProblemsFilter

View file

@ -8,8 +8,6 @@ using GodotTools.Internals;
using static GodotTools.Internals.Globals; using static GodotTools.Internals.Globals;
using FileAccess = Godot.FileAccess; using FileAccess = Godot.FileAccess;
#nullable enable
namespace GodotTools.Build namespace GodotTools.Build
{ {
public partial class BuildProblemsView : HBoxContainer public partial class BuildProblemsView : HBoxContainer
@ -244,7 +242,9 @@ namespace GodotTools.Build
if (string.IsNullOrEmpty(projectDir)) if (string.IsNullOrEmpty(projectDir))
return; return;
string file = Path.Combine(projectDir.SimplifyGodotPath(), diagnostic.File.SimplifyGodotPath()); string? file = !string.IsNullOrEmpty(diagnostic.File) ?
Path.Combine(projectDir.SimplifyGodotPath(), diagnostic.File.SimplifyGodotPath()) :
null;
if (!File.Exists(file)) if (!File.Exists(file))
return; return;

View file

@ -17,10 +17,10 @@ namespace GodotTools.Build
{ {
public static class BuildSystem public static class BuildSystem
{ {
private static Process LaunchBuild(BuildInfo buildInfo, Action<string> stdOutHandler, private static Process LaunchBuild(BuildInfo buildInfo, Action<string?>? stdOutHandler,
Action<string> stdErrHandler) Action<string?>? stdErrHandler)
{ {
string dotnetPath = DotNetFinder.FindDotNetExe(); string? dotnetPath = DotNetFinder.FindDotNetExe();
if (dotnetPath == null) if (dotnetPath == null)
throw new FileNotFoundException("Cannot find the dotnet executable."); throw new FileNotFoundException("Cannot find the dotnet executable.");
@ -67,7 +67,7 @@ namespace GodotTools.Build
return process; return process;
} }
public static int Build(BuildInfo buildInfo, Action<string> stdOutHandler, Action<string> stdErrHandler) public static int Build(BuildInfo buildInfo, Action<string?>? stdOutHandler, Action<string?>? stdErrHandler)
{ {
using (var process = LaunchBuild(buildInfo, stdOutHandler, stdErrHandler)) using (var process = LaunchBuild(buildInfo, stdOutHandler, stdErrHandler))
{ {
@ -77,8 +77,8 @@ namespace GodotTools.Build
} }
} }
public static async Task<int> BuildAsync(BuildInfo buildInfo, Action<string> stdOutHandler, public static async Task<int> BuildAsync(BuildInfo buildInfo, Action<string?>? stdOutHandler,
Action<string> stdErrHandler) Action<string?>? stdErrHandler)
{ {
using (var process = LaunchBuild(buildInfo, stdOutHandler, stdErrHandler)) using (var process = LaunchBuild(buildInfo, stdOutHandler, stdErrHandler))
{ {
@ -88,10 +88,10 @@ namespace GodotTools.Build
} }
} }
private static Process LaunchPublish(BuildInfo buildInfo, Action<string> stdOutHandler, private static Process LaunchPublish(BuildInfo buildInfo, Action<string?>? stdOutHandler,
Action<string> stdErrHandler) Action<string?>? stdErrHandler)
{ {
string dotnetPath = DotNetFinder.FindDotNetExe(); string? dotnetPath = DotNetFinder.FindDotNetExe();
if (dotnetPath == null) if (dotnetPath == null)
throw new FileNotFoundException("Cannot find the dotnet executable."); throw new FileNotFoundException("Cannot find the dotnet executable.");
@ -137,7 +137,7 @@ namespace GodotTools.Build
return process; return process;
} }
public static int Publish(BuildInfo buildInfo, Action<string> stdOutHandler, Action<string> stdErrHandler) public static int Publish(BuildInfo buildInfo, Action<string?>? stdOutHandler, Action<string?>? stdErrHandler)
{ {
using (var process = LaunchPublish(buildInfo, stdOutHandler, stdErrHandler)) using (var process = LaunchPublish(buildInfo, stdOutHandler, stdErrHandler))
{ {
@ -297,7 +297,7 @@ namespace GodotTools.Build
} }
private static Process DoGenerateXCFramework(List<string> outputPaths, string xcFrameworkPath, private static Process DoGenerateXCFramework(List<string> outputPaths, string xcFrameworkPath,
Action<string> stdOutHandler, Action<string> stdErrHandler) Action<string?>? stdOutHandler, Action<string?>? stdErrHandler)
{ {
if (Directory.Exists(xcFrameworkPath)) if (Directory.Exists(xcFrameworkPath))
{ {
@ -341,7 +341,7 @@ namespace GodotTools.Build
return process; return process;
} }
public static int GenerateXCFramework(List<string> outputPaths, string xcFrameworkPath, Action<string> stdOutHandler, Action<string> stdErrHandler) public static int GenerateXCFramework(List<string> outputPaths, string xcFrameworkPath, Action<string?>? stdOutHandler, Action<string?>? stdErrHandler)
{ {
using (var process = DoGenerateXCFramework(outputPaths, xcFrameworkPath, stdOutHandler, stdErrHandler)) using (var process = DoGenerateXCFramework(outputPaths, xcFrameworkPath, stdOutHandler, stdErrHandler))
{ {

View file

@ -5,15 +5,13 @@ using System.Diagnostics.CodeAnalysis;
using System.IO; using System.IO;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text; using System.Text;
using JetBrains.Annotations;
using OS = GodotTools.Utils.OS; using OS = GodotTools.Utils.OS;
namespace GodotTools.Build namespace GodotTools.Build
{ {
public static class DotNetFinder public static class DotNetFinder
{ {
[CanBeNull] public static string? FindDotNetExe()
public static string FindDotNetExe()
{ {
// In the future, this method may do more than just search in PATH. We could look in // In the future, this method may do more than just search in PATH. We could look in
// known locations or use Godot's linked nethost to search from the hostfxr location. // known locations or use Godot's linked nethost to search from the hostfxr location.
@ -40,14 +38,14 @@ namespace GodotTools.Build
public static bool TryFindDotNetSdk( public static bool TryFindDotNetSdk(
Version expectedVersion, Version expectedVersion,
[NotNullWhen(true)] out Version version, [NotNullWhen(true)] out Version? version,
[NotNullWhen(true)] out string path [NotNullWhen(true)] out string? path
) )
{ {
version = null; version = null;
path = null; path = null;
string dotNetExe = FindDotNetExe(); string? dotNetExe = FindDotNetExe();
if (string.IsNullOrEmpty(dotNetExe)) if (string.IsNullOrEmpty(dotNetExe))
return false; return false;
@ -86,8 +84,8 @@ namespace GodotTools.Build
process.BeginOutputReadLine(); process.BeginOutputReadLine();
process.WaitForExit(); process.WaitForExit();
Version latestVersionMatch = null; Version? latestVersionMatch = null;
string matchPath = null; string? matchPath = null;
foreach (var line in lines) foreach (var line in lines)
{ {

View file

@ -5,8 +5,6 @@ using GodotTools.Internals;
using static GodotTools.Internals.Globals; using static GodotTools.Internals.Globals;
using File = GodotTools.Utils.File; using File = GodotTools.Utils.File;
#nullable enable
namespace GodotTools.Build namespace GodotTools.Build
{ {
public partial class MSBuildPanel : MarginContainer, ISerializationListener public partial class MSBuildPanel : MarginContainer, ISerializationListener
@ -183,7 +181,7 @@ namespace GodotTools.Build
} }
} }
private void StdOutputReceived(string text) private void StdOutputReceived(string? text)
{ {
lock (_pendingBuildLogTextLock) lock (_pendingBuildLogTextLock)
{ {
@ -193,7 +191,7 @@ namespace GodotTools.Build
} }
} }
private void StdErrorReceived(string text) private void StdErrorReceived(string? text)
{ {
lock (_pendingBuildLogTextLock) lock (_pendingBuildLogTextLock)
{ {

View file

@ -1,6 +1,7 @@
using Godot; using Godot;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Security.Cryptography; using System.Security.Cryptography;
@ -73,7 +74,7 @@ namespace GodotTools.Export
}; };
} }
private string _maybeLastExportError; private string? _maybeLastExportError;
// With this method we can override how a file is exported in the PCK // With this method we can override how a file is exported in the PCK
public override void _ExportFile(string path, string type, string[] features) public override void _ExportFile(string path, string type, string[] features)
@ -135,7 +136,7 @@ namespace GodotTools.Export
if (!ProjectContainsDotNet()) if (!ProjectContainsDotNet())
return; return;
if (!DeterminePlatformFromFeatures(features, out string platform)) if (!DeterminePlatformFromFeatures(features, out string? platform))
throw new NotSupportedException("Target platform not supported."); throw new NotSupportedException("Target platform not supported.");
if (!new[] { OS.Platforms.Windows, OS.Platforms.LinuxBSD, OS.Platforms.MacOS, OS.Platforms.Android, OS.Platforms.iOS } if (!new[] { OS.Platforms.Windows, OS.Platforms.LinuxBSD, OS.Platforms.MacOS, OS.Platforms.Android, OS.Platforms.iOS }
@ -327,7 +328,7 @@ namespace GodotTools.Export
AddSharedObject(path, tags: null, AddSharedObject(path, tags: null,
Path.Join(projectDataDirName, Path.Join(projectDataDirName,
Path.GetRelativePath(publishOutputDir, Path.GetRelativePath(publishOutputDir,
Path.GetDirectoryName(path)))); Path.GetDirectoryName(path)!)));
} }
} }
} }
@ -450,7 +451,7 @@ namespace GodotTools.Export
} }
} }
private static bool DeterminePlatformFromFeatures(IEnumerable<string> features, out string platform) private static bool DeterminePlatformFromFeatures(IEnumerable<string> features, [NotNullWhen(true)] out string? platform)
{ {
foreach (var feature in features) foreach (var feature in features)
{ {

View file

@ -5,7 +5,7 @@ namespace GodotTools.Export
{ {
public static class XcodeHelper public static class XcodeHelper
{ {
private static string _XcodePath = null; private static string? _XcodePath = null;
public static string XcodePath public static string XcodePath
{ {
@ -23,7 +23,7 @@ namespace GodotTools.Export
} }
} }
private static string FindSelectedXcode() private static string? FindSelectedXcode()
{ {
var outputWrapper = new Godot.Collections.Array(); var outputWrapper = new Godot.Collections.Array();
@ -40,9 +40,9 @@ namespace GodotTools.Export
return null; return null;
} }
public static string FindXcode() public static string? FindXcode()
{ {
string selectedXcode = FindSelectedXcode(); string? selectedXcode = FindSelectedXcode();
if (selectedXcode != null) if (selectedXcode != null)
{ {
if (Directory.Exists(Path.Combine(selectedXcode, "Contents", "Developer"))) if (Directory.Exists(Path.Combine(selectedXcode, "Contents", "Developer")))
@ -50,10 +50,10 @@ namespace GodotTools.Export
// The path already pointed to Contents/Developer // The path already pointed to Contents/Developer
var dirInfo = new DirectoryInfo(selectedXcode); var dirInfo = new DirectoryInfo(selectedXcode);
if (dirInfo.Name != "Developer" || dirInfo.Parent.Name != "Contents") if (dirInfo is not { Parent.Name: "Contents", Name: "Developer" })
{ {
Console.WriteLine(Path.GetDirectoryName(selectedXcode)); Console.WriteLine(Path.GetDirectoryName(selectedXcode));
Console.WriteLine(System.IO.Directory.GetParent(selectedXcode).Name); Console.WriteLine(System.IO.Directory.GetParent(selectedXcode)?.Name);
Console.Error.WriteLine("Unrecognized path for selected Xcode"); Console.Error.WriteLine("Unrecognized path for selected Xcode");
} }
else else

View file

@ -34,6 +34,7 @@ namespace GodotTools
public const string ProblemsLayout = "dotnet/build/problems_layout"; public const string ProblemsLayout = "dotnet/build/problems_layout";
} }
#nullable disable
private EditorSettings _editorSettings; private EditorSettings _editorSettings;
private PopupMenu _menuPopup; private PopupMenu _menuPopup;
@ -51,6 +52,7 @@ namespace GodotTools
public GodotIdeManager GodotIdeManager { get; private set; } public GodotIdeManager GodotIdeManager { get; private set; }
public MSBuildPanel MSBuildPanel { get; private set; } public MSBuildPanel MSBuildPanel { get; private set; }
#nullable enable
public bool SkipBuildBeforePlaying { get; set; } = false; public bool SkipBuildBeforePlaying { get; set; } = false;
@ -67,29 +69,23 @@ namespace GodotTools
private bool CreateProjectSolution() private bool CreateProjectSolution()
{ {
string errorMessage = null; string? errorMessage = null;
using (var pr = new EditorProgress("create_csharp_solution", "Generating solution...".TTR(), 2)) using (var pr = new EditorProgress("create_csharp_solution", "Generating solution...".TTR(), 2))
{ {
pr.Step("Generating C# project...".TTR()); pr.Step("Generating C# project...".TTR());
string csprojDir = Path.GetDirectoryName(GodotSharpDirs.ProjectCsProjPath); string csprojDir = Path.GetDirectoryName(GodotSharpDirs.ProjectCsProjPath)!;
string slnDir = Path.GetDirectoryName(GodotSharpDirs.ProjectSlnPath); string slnDir = Path.GetDirectoryName(GodotSharpDirs.ProjectSlnPath)!;
string name = GodotSharpDirs.ProjectAssemblyName; string name = GodotSharpDirs.ProjectAssemblyName;
string guid = CsProjOperations.GenerateGameProject(csprojDir, name); string guid = CsProjOperations.GenerateGameProject(csprojDir, name);
if (guid.Length > 0) if (guid.Length > 0)
{ {
var solution = new DotNetSolution(name) var solution = new DotNetSolution(name, slnDir);
{
DirectoryPath = slnDir
};
var projectInfo = new DotNetSolution.ProjectInfo var projectInfo = new DotNetSolution.ProjectInfo(guid,
{ Path.GetRelativePath(slnDir, GodotSharpDirs.ProjectCsProjPath),
Guid = guid, new List<string> { "Debug", "ExportDebug", "ExportRelease" });
PathRelativeToSolution = Path.GetRelativePath(slnDir, GodotSharpDirs.ProjectCsProjPath),
Configs = new List<string> { "Debug", "ExportDebug", "ExportRelease" }
};
solution.AddNewProject(name, projectInfo); solution.AddNewProject(name, projectInfo);
@ -342,7 +338,7 @@ namespace GodotTools
} }
} }
args.Add(Path.GetDirectoryName(GodotSharpDirs.ProjectSlnPath)); args.Add(Path.GetDirectoryName(GodotSharpDirs.ProjectSlnPath)!);
string scriptPath = ProjectSettings.GlobalizePath(script.ResourcePath); string scriptPath = ProjectSettings.GlobalizePath(script.ResourcePath);
@ -459,7 +455,7 @@ namespace GodotTools
// First we try to find the .NET Sdk ourselves to make sure we get the // First we try to find the .NET Sdk ourselves to make sure we get the
// correct version first, otherwise pick the latest. // correct version first, otherwise pick the latest.
if (DotNetFinder.TryFindDotNetSdk(dotNetSdkSearchVersion, out var sdkVersion, out string sdkPath)) if (DotNetFinder.TryFindDotNetSdk(dotNetSdkSearchVersion, out var sdkVersion, out string? sdkPath))
{ {
if (Godot.OS.IsStdOutVerbose()) if (Godot.OS.IsStdOutVerbose())
Console.WriteLine($"Found .NET Sdk version '{sdkVersion}': {sdkPath}"); Console.WriteLine($"Found .NET Sdk version '{sdkVersion}': {sdkPath}");
@ -694,8 +690,9 @@ namespace GodotTools
} }
// Singleton // Singleton
#nullable disable
public static GodotSharpEditor Instance { get; private set; } public static GodotSharpEditor Instance { get; private set; }
#nullable enable
[UsedImplicitly] [UsedImplicitly]
private static IntPtr InternalCreateInstance(IntPtr unmanagedCallbacks, int unmanagedCallbacksSize) private static IntPtr InternalCreateInstance(IntPtr unmanagedCallbacks, int unmanagedCallbacksSize)

View file

@ -4,6 +4,7 @@
<TargetFramework>net6.0</TargetFramework> <TargetFramework>net6.0</TargetFramework>
<EnableDynamicLoading>true</EnableDynamicLoading> <EnableDynamicLoading>true</EnableDynamicLoading>
<LangVersion>10</LangVersion> <LangVersion>10</LangVersion>
<Nullable>enable</Nullable>
<!-- The Godot editor uses the Debug Godot API assemblies --> <!-- The Godot editor uses the Debug Godot API assemblies -->
<GodotApiConfiguration>Debug</GodotApiConfiguration> <GodotApiConfiguration>Debug</GodotApiConfiguration>
<GodotSourceRootPath>$(SolutionDir)/../../../../</GodotSourceRootPath> <GodotSourceRootPath>$(SolutionDir)/../../../../</GodotSourceRootPath>

View file

@ -7,7 +7,9 @@ namespace GodotTools
{ {
public partial class HotReloadAssemblyWatcher : Node public partial class HotReloadAssemblyWatcher : Node
{ {
#nullable disable
private Timer _watchTimer; private Timer _watchTimer;
#nullable enable
public override void _Notification(int what) public override void _Notification(int what)
{ {

View file

@ -10,10 +10,10 @@ namespace GodotTools.Ides
{ {
public sealed partial class GodotIdeManager : Node, ISerializationListener public sealed partial class GodotIdeManager : Node, ISerializationListener
{ {
private MessagingServer _messagingServer; private MessagingServer? _messagingServer;
private MonoDevelop.Instance _monoDevelInstance; private MonoDevelop.Instance? _monoDevelInstance;
private MonoDevelop.Instance _vsForMacInstance; private MonoDevelop.Instance? _vsForMacInstance;
private MessagingServer GetRunningOrNewServer() private MessagingServer GetRunningOrNewServer()
{ {
@ -59,7 +59,7 @@ namespace GodotTools.Ides
switch (editorId) switch (editorId)
{ {
case ExternalEditorId.None: case ExternalEditorId.None:
return null; return string.Empty;
case ExternalEditorId.VisualStudio: case ExternalEditorId.VisualStudio:
return "VisualStudio"; return "VisualStudio";
case ExternalEditorId.VsCode: case ExternalEditorId.VsCode:

View file

@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
@ -97,9 +98,9 @@ namespace GodotTools.Ides
foreach (var connection in Peers) foreach (var connection in Peers)
connection.Dispose(); connection.Dispose();
Peers.Clear(); Peers.Clear();
_listener?.Stop(); _listener.Stop();
_metaFile?.Dispose(); _metaFile.Dispose();
File.Delete(_metaFilePath); File.Delete(_metaFilePath);
} }
@ -122,7 +123,7 @@ namespace GodotTools.Ides
_listener = new TcpListener(new IPEndPoint(IPAddress.Loopback, port: 0)); _listener = new TcpListener(new IPEndPoint(IPAddress.Loopback, port: 0));
_listener.Start(); _listener.Start();
int port = ((IPEndPoint)_listener.Server.LocalEndPoint).Port; int port = ((IPEndPoint?)_listener.Server.LocalEndPoint)?.Port ?? 0;
using (var metaFileWriter = new StreamWriter(_metaFile, Encoding.UTF8)) using (var metaFileWriter = new StreamWriter(_metaFile, Encoding.UTF8))
{ {
metaFileWriter.WriteLine(port); metaFileWriter.WriteLine(port);
@ -235,7 +236,7 @@ namespace GodotTools.Ides
public string GetHandshakeLine(string identity) => $"{_serverHandshakeBase},{identity}"; public string GetHandshakeLine(string identity) => $"{_serverHandshakeBase},{identity}";
public bool IsValidPeerHandshake(string handshake, out string identity, ILogger logger) public bool IsValidPeerHandshake(string handshake, [NotNullWhen(true)] out string? identity, ILogger logger)
{ {
identity = null; identity = null;
@ -311,12 +312,12 @@ namespace GodotTools.Ides
[DebugPlayRequest.Id] = async (peer, content) => [DebugPlayRequest.Id] = async (peer, content) =>
{ {
var request = JsonConvert.DeserializeObject<DebugPlayRequest>(content.Body); var request = JsonConvert.DeserializeObject<DebugPlayRequest>(content.Body);
return await HandleDebugPlay(request); return await HandleDebugPlay(request!);
}, },
[StopPlayRequest.Id] = async (peer, content) => [StopPlayRequest.Id] = async (peer, content) =>
{ {
var request = JsonConvert.DeserializeObject<StopPlayRequest>(content.Body); var request = JsonConvert.DeserializeObject<StopPlayRequest>(content.Body);
return await HandleStopPlay(request); return await HandleStopPlay(request!);
}, },
[ReloadScriptsRequest.Id] = async (peer, content) => [ReloadScriptsRequest.Id] = async (peer, content) =>
{ {
@ -326,7 +327,7 @@ namespace GodotTools.Ides
[CodeCompletionRequest.Id] = async (peer, content) => [CodeCompletionRequest.Id] = async (peer, content) =>
{ {
var request = JsonConvert.DeserializeObject<CodeCompletionRequest>(content.Body); var request = JsonConvert.DeserializeObject<CodeCompletionRequest>(content.Body);
return await HandleCodeCompletionRequest(request); return await HandleCodeCompletionRequest(request!);
} }
}; };
} }
@ -383,7 +384,7 @@ namespace GodotTools.Ides
{ {
// This is needed if the "resource path" part of the path is case insensitive. // This is needed if the "resource path" part of the path is case insensitive.
// However, it doesn't fix resource loading if the rest of the path is also case insensitive. // However, it doesn't fix resource loading if the rest of the path is also case insensitive.
string scriptFileLocalized = FsPathUtils.LocalizePathWithCaseChecked(request.ScriptFile); string? scriptFileLocalized = FsPathUtils.LocalizePathWithCaseChecked(request.ScriptFile);
// The node API can only be called from the main thread. // The node API can only be called from the main thread.
await Godot.Engine.GetMainLoop().ToSignal(Godot.Engine.GetMainLoop(), "process_frame"); await Godot.Engine.GetMainLoop().ToSignal(Godot.Engine.GetMainLoop(), "process_frame");

View file

@ -13,7 +13,7 @@ namespace GodotTools.Ides.MonoDevelop
private readonly string _solutionFile; private readonly string _solutionFile;
private readonly EditorId _editorId; private readonly EditorId _editorId;
private Process _process; private Process? _process;
public bool IsRunning => _process != null && !_process.HasExited; public bool IsRunning => _process != null && !_process.HasExited;
public bool IsDisposed { get; private set; } public bool IsDisposed { get; private set; }
@ -24,7 +24,7 @@ namespace GodotTools.Ides.MonoDevelop
var args = new List<string>(); var args = new List<string>();
string command; string? command;
if (OS.IsMacOS) if (OS.IsMacOS)
{ {
@ -136,6 +136,8 @@ namespace GodotTools.Ides.MonoDevelop
{EditorId.MonoDevelop, "monodevelop"} {EditorId.MonoDevelop, "monodevelop"}
}; };
} }
ExecutableNames ??= new Dictionary<EditorId, string>();
BundleIds ??= new Dictionary<EditorId, string>();
} }
} }
} }

View file

@ -20,12 +20,12 @@ public class RiderLocatorEnvironment : IRiderLocatorEnvironment
} }
} }
public T FromJson<T>(string json) public T? FromJson<T>(string json)
{ {
return JsonConvert.DeserializeObject<T>(json); return JsonConvert.DeserializeObject<T>(json);
} }
public void Info(string message, Exception e = null) public void Info(string message, Exception? e = null)
{ {
if (e == null) if (e == null)
GD.Print(message); GD.Print(message);
@ -33,7 +33,7 @@ public class RiderLocatorEnvironment : IRiderLocatorEnvironment
GD.Print(message, e); GD.Print(message, e);
} }
public void Warn(string message, Exception e = null) public void Warn(string message, Exception? e = null)
{ {
if (e == null) if (e == null)
GD.PushWarning(message); GD.PushWarning(message);
@ -41,7 +41,7 @@ public class RiderLocatorEnvironment : IRiderLocatorEnvironment
GD.PushWarning(message, e); GD.PushWarning(message, e);
} }
public void Error(string message, Exception e = null) public void Error(string message, Exception? e = null)
{ {
if (e == null) if (e == null)
GD.PushError(message); GD.PushError(message);
@ -49,7 +49,7 @@ public class RiderLocatorEnvironment : IRiderLocatorEnvironment
GD.PushError(message, e); GD.PushError(message, e);
} }
public void Verbose(string message, Exception e = null) public void Verbose(string message, Exception? e = null)
{ {
// do nothing, since IDK how to write only to the log, without spamming the output // do nothing, since IDK how to write only to the log, without spamming the output
} }

View file

@ -5,8 +5,6 @@ using Godot;
using GodotTools.Internals; using GodotTools.Internals;
using JetBrains.Rider.PathLocator; using JetBrains.Rider.PathLocator;
#nullable enable
namespace GodotTools.Ides.Rider namespace GodotTools.Ides.Rider
{ {
public static class RiderPathManager public static class RiderPathManager

View file

@ -1,3 +1,4 @@
using System.Diagnostics.CodeAnalysis;
using System.IO; using System.IO;
using Godot; using Godot;
using Godot.NativeInterop; using Godot.NativeInterop;
@ -59,16 +60,17 @@ namespace GodotTools.Internals
} }
} }
[MemberNotNull("_projectAssemblyName", "_projectSlnPath", "_projectCsProjPath")]
public static void DetermineProjectLocation() public static void DetermineProjectLocation()
{ {
_projectAssemblyName = (string)ProjectSettings.GetSetting("dotnet/project/assembly_name"); _projectAssemblyName = (string?)ProjectSettings.GetSetting("dotnet/project/assembly_name");
if (string.IsNullOrEmpty(_projectAssemblyName)) if (string.IsNullOrEmpty(_projectAssemblyName))
{ {
_projectAssemblyName = CSharpProjectName; _projectAssemblyName = CSharpProjectName;
ProjectSettings.SetSetting("dotnet/project/assembly_name", _projectAssemblyName); ProjectSettings.SetSetting("dotnet/project/assembly_name", _projectAssemblyName);
} }
string slnParentDir = (string)ProjectSettings.GetSetting("dotnet/project/solution_directory"); string? slnParentDir = (string?)ProjectSettings.GetSetting("dotnet/project/solution_directory");
if (string.IsNullOrEmpty(slnParentDir)) if (string.IsNullOrEmpty(slnParentDir))
slnParentDir = "res://"; slnParentDir = "res://";
else if (!slnParentDir.StartsWith("res://")) else if (!slnParentDir.StartsWith("res://"))
@ -84,9 +86,9 @@ namespace GodotTools.Internals
string.Concat(_projectAssemblyName, ".csproj")); string.Concat(_projectAssemblyName, ".csproj"));
} }
private static string _projectAssemblyName; private static string? _projectAssemblyName;
private static string _projectSlnPath; private static string? _projectSlnPath;
private static string _projectCsProjPath; private static string? _projectCsProjPath;
public static string ProjectAssemblyName public static string ProjectAssemblyName
{ {

View file

@ -1,17 +1,19 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO; using System.IO;
namespace GodotTools.Utils namespace GodotTools.Utils
{ {
public static class CollectionExtensions public static class CollectionExtensions
{ {
public static T SelectFirstNotNull<T>(this IEnumerable<T> enumerable, Func<T, T> predicate, T orElse = null) [return: NotNullIfNotNull("orElse")]
public static T? SelectFirstNotNull<T>(this IEnumerable<T> enumerable, Func<T, T?> predicate, T? orElse = null)
where T : class where T : class
{ {
foreach (T elem in enumerable) foreach (T elem in enumerable)
{ {
T result = predicate(elem); T? result = predicate(elem);
if (result != null) if (result != null)
return result; return result;
} }
@ -21,7 +23,7 @@ namespace GodotTools.Utils
public static IEnumerable<string> EnumerateLines(this TextReader textReader) public static IEnumerable<string> EnumerateLines(this TextReader textReader)
{ {
string line; string? line;
while ((line = textReader.ReadLine()) != null) while ((line = textReader.ReadLine()) != null)
yield return line; yield return line;
} }

View file

@ -30,8 +30,7 @@ namespace GodotTools.Utils
return childPathNorm.PathStartsWithAlreadyNorm(parentPathNorm); return childPathNorm.PathStartsWithAlreadyNorm(parentPathNorm);
} }
[return: MaybeNull] public static string? LocalizePathWithCaseChecked(string path)
public static string LocalizePathWithCaseChecked(string path)
{ {
string pathNorm = path.NormalizePath() + Path.DirectorySeparatorChar; string pathNorm = path.NormalizePath() + Path.DirectorySeparatorChar;
string resourcePathNorm = ResourcePath.NormalizePath() + Path.DirectorySeparatorChar; string resourcePathNorm = ResourcePath.NormalizePath() + Path.DirectorySeparatorChar;

View file

@ -150,8 +150,7 @@ namespace GodotTools.Utils
public static char PathSep => IsWindows ? ';' : ':'; public static char PathSep => IsWindows ? ';' : ':';
[return: MaybeNull] public static string? PathWhich(string name)
public static string PathWhich([NotNull] string name)
{ {
if (IsWindows) if (IsWindows)
return PathWhichWindows(name); return PathWhichWindows(name);
@ -159,12 +158,11 @@ namespace GodotTools.Utils
return PathWhichUnix(name); return PathWhichUnix(name);
} }
[return: MaybeNull] private static string? PathWhichWindows(string name)
private static string PathWhichWindows([NotNull] string name)
{ {
string[] windowsExts = string[] windowsExts =
Environment.GetEnvironmentVariable("PATHEXT")?.Split(PathSep) ?? Array.Empty<string>(); Environment.GetEnvironmentVariable("PATHEXT")?.Split(PathSep) ?? Array.Empty<string>();
string[] pathDirs = Environment.GetEnvironmentVariable("PATH")?.Split(PathSep); string[]? pathDirs = Environment.GetEnvironmentVariable("PATH")?.Split(PathSep);
char[] invalidPathChars = Path.GetInvalidPathChars(); char[] invalidPathChars = Path.GetInvalidPathChars();
var searchDirs = new List<string>(); var searchDirs = new List<string>();
@ -196,10 +194,9 @@ namespace GodotTools.Utils
select path + ext).FirstOrDefault(File.Exists); select path + ext).FirstOrDefault(File.Exists);
} }
[return: MaybeNull] private static string? PathWhichUnix(string name)
private static string PathWhichUnix([NotNull] string name)
{ {
string[] pathDirs = Environment.GetEnvironmentVariable("PATH")?.Split(PathSep); string[]? pathDirs = Environment.GetEnvironmentVariable("PATH")?.Split(PathSep);
char[] invalidPathChars = Path.GetInvalidPathChars(); char[] invalidPathChars = Path.GetInvalidPathChars();
var searchDirs = new List<string>(); var searchDirs = new List<string>();
@ -238,7 +235,7 @@ namespace GodotTools.Utils
foreach (string arg in arguments) foreach (string arg in arguments)
startInfo.ArgumentList.Add(arg); startInfo.ArgumentList.Add(arg);
using Process process = Process.Start(startInfo); using Process? process = Process.Start(startInfo);
if (process == null) if (process == null)
throw new InvalidOperationException("No process was started."); throw new InvalidOperationException("No process was started.");
@ -315,7 +312,7 @@ namespace GodotTools.Utils
public static StringBuilder GetCommandLineDisplay( public static StringBuilder GetCommandLineDisplay(
this ProcessStartInfo startInfo, this ProcessStartInfo startInfo,
StringBuilder optionalBuilder = null StringBuilder? optionalBuilder = null
) )
{ {
var builder = optionalBuilder ?? new StringBuilder(); var builder = optionalBuilder ?? new StringBuilder();