Fix issues reported in PVS analysis (#4406)

* Fix issues reported in PVS analysis

https://pvs-studio.com/en/blog/posts/csharp/1356/

* Initial version of concmd argument autocomplete/hints

https://files.facepunch.com/lolleko/2026/March/27_09-38-ApprehensiveGermanshorthairedpointer.mp4
This commit is contained in:
Lorenz Junglas
2026-05-08 14:19:55 +01:00
committed by GitHub
parent 5b14227991
commit 12e3ad13aa
10 changed files with 115 additions and 36 deletions

View File

@@ -110,9 +110,6 @@ internal class MountHost : IDisposable
internal void UnregisterTypes( Assembly assembly )
{
var assetSourceType = typeof( BaseGameMount );
var types = assembly.GetTypes().Where( t => assetSourceType.IsAssignableFrom( t ) && !t.IsAbstract );
foreach ( var (ident, source) in Sources.ToArray() )
{
if ( source.GetType().Assembly != assembly ) continue;

View File

@@ -239,6 +239,6 @@ public partial class Bitmap
return false;
var header = Encoding.ASCII.GetString( data, 0, Math.Min( 20, data.Length ) ).Trim();
return header.StartsWith( "IESNA" ) || header.StartsWith( "IESNA:LM-63" );
return header.StartsWith( "IESNA" );
}
}

View File

@@ -2090,7 +2090,7 @@ internal sealed partial class Mesh
// Disconnect the edge that is being collapsed from the faces and other edges.
Assert.True( hEdgeA.IsValid && hEdgeB.IsValid );
if ( hEdgeA.IsValid && hEdgeA.IsValid )
if ( hEdgeA.IsValid && hEdgeB.IsValid )
{
var pNewVertex = hNewVertex;
var hNextEdgeA = hEdgeA.NextEdge;

View File

@@ -2,39 +2,120 @@
internal static partial class ConVarSystem
{
// [name=default] for optional params, <name:Type> for required.
static string FormatParamHint( System.Reflection.ParameterInfo p )
=> p.HasDefaultValue
? $"[{p.Name}={p.DefaultValue}]"
: $"<{p.Name}:{p.ParameterType.Name}>";
// Space-separated hints for all params from fromIndex onward.
static string BuildRemainingHint( System.Reflection.ParameterInfo[] parameters, int fromIndex )
{
if ( fromIndex >= parameters.Length ) return string.Empty;
return string.Join( " ", parameters[fromIndex..].Select( FormatParamHint ) );
}
public static ConCmdAttribute.AutoCompleteResult[] GetAutoComplete( string partial, int count )
{
var parts = partial.SplitQuotesStrings();
return partial.Contains( ' ' )
? GetArgumentAutoComplete( partial, parts, count )
: GetCommandAutoComplete( partial, parts, count );
}
// Completes argument values once a command name has been typed.
static ConCmdAttribute.AutoCompleteResult[] GetArgumentAutoComplete( string partial, string[] parts, int count )
{
if ( !Members.TryGetValue( parts[0], out var command ) )
return Array.Empty<ConCmdAttribute.AutoCompleteResult>();
if ( command is not ManagedCommand managed )
return Array.Empty<ConCmdAttribute.AutoCompleteResult>();
// Connection is injected at call time, not supplied by the user.
var paramOffset = managed.parameters.Length > 0 && managed.parameters[0].ParameterType == typeof( Connection ) ? 1 : 0;
// Trailing space → user started a new arg. No trailing space → still mid-token.
var startedNewArg = partial[^1] == ' ';
var argIndex = startedNewArg ? parts.Length - 1 : parts.Length - 2;
var partialArg = startedNewArg ? "" : parts[^1];
var commandPrefix = startedNewArg ? partial.TrimEnd() : string.Join( " ", parts[..^1] );
var paramIndex = paramOffset + argIndex;
if ( paramIndex >= managed.parameters.Length )
return Array.Empty<ConCmdAttribute.AutoCompleteResult>();
var param = managed.parameters[paramIndex];
var remainingHint = BuildRemainingHint( managed.parameters, paramIndex + 1 );
IEnumerable<string> suggestions = param.ParameterType switch
{
var t when t == typeof( bool ) => ["true", "false"],
var t when t.IsEnum => Enum.GetNames( t ),
_ => null,
};
List<ConCmdAttribute.AutoCompleteResult> results = new();
//
// if we have more than one part, complete a specific command
//
if ( parts.Length > 1 )
if ( suggestions is not null )
{
if ( !Members.TryGetValue( parts[0], out var command ) )
return Array.Empty<ConCmdAttribute.AutoCompleteResult>();
//results.Add( new ConCmd.AutoCompleteResult { Command = command.Name, Description = command.Help } );
// TODO - dig into it for auto complete
return results.Take( count ).ToArray();
foreach ( var s in suggestions
.Where( s => s.StartsWith( partialArg, StringComparison.OrdinalIgnoreCase ) )
.Take( count ) )
{
var cmd = string.IsNullOrEmpty( remainingHint )
? $"{commandPrefix} {s}"
: $"{commandPrefix} {s} {remainingHint}";
results.Add( new ConCmdAttribute.AutoCompleteResult
{
Command = cmd,
Description = $"{param.Name} ({param.ParameterType.Name})",
} );
}
}
else
{
// Non-enum/bool: show a type hint so the user knows what to type.
var currentHint = FormatParamHint( param );
var fullCmd = string.IsNullOrEmpty( remainingHint )
? $"{commandPrefix} {currentHint}"
: $"{commandPrefix} {currentHint} {remainingHint}";
results.Add( new ConCmdAttribute.AutoCompleteResult
{
Command = fullCmd,
Description = $"{param.Name} ({param.ParameterType.Name})",
} );
}
//
// Find the command starting with this
//
return results.ToArray();
}
// Completes command names, and for an exact match shows the full parameter signature.
static ConCmdAttribute.AutoCompleteResult[] GetCommandAutoComplete( string partial, string[] parts, int count )
{
List<ConCmdAttribute.AutoCompleteResult> results = new();
foreach ( var option in Members.Values
.Where( x => !x.IsHidden )
.Where( x => x.Name.StartsWith( partial, StringComparison.OrdinalIgnoreCase ) )
.OrderBy( x => x.Name ) )
.Where( x => !x.IsHidden )
.Where( x => x.Name.StartsWith( partial, StringComparison.OrdinalIgnoreCase ) )
.OrderBy( x => x.Name ) )
{
if ( option.Name == partial )
if ( string.Equals( option.Name, partial, StringComparison.OrdinalIgnoreCase )
&& option is ManagedCommand exactManaged )
{
var paramOffset = exactManaged.parameters.Length > 0 && exactManaged.parameters[0].ParameterType == typeof( Connection ) ? 1 : 0;
var hint = BuildRemainingHint( exactManaged.parameters, paramOffset );
if ( !string.IsNullOrEmpty( hint ) )
{
results.Add( new ConCmdAttribute.AutoCompleteResult
{
Command = $"{option.Name} {hint}",
Description = option.BuildDescription(),
} );
}
continue;
}
results.Add( new ConCmdAttribute.AutoCompleteResult
{

View File

@@ -18,7 +18,7 @@ internal sealed partial class Controller
internal Input.Context InputContext { get; set; }
/// SDL reports values between this range
static readonly Vector2 AXIS_RANGE = new( -32768, 32767 );
internal static readonly Vector2 AXIS_RANGE = new( -32768, 32767 );
List<InputAxis> ControllerAxes { get; set; } = new();

View File

@@ -200,7 +200,8 @@ internal static partial class InputRouter
_ => GamepadCode.None,
};
OnGamepadCode( deviceId, code, value >= triggerDeadzone );
// Normalize raw SDL axis value to 0-1 range before comparing against the normalized deadzone.
OnGamepadCode( deviceId, code, ((float)value).Remap( 0, Controller.AXIS_RANGE.y, 0, 1 ) >= triggerDeadzone );
}
internal static void OnGameControllerConnected( int joystickId, int deviceId )

View File

@@ -99,10 +99,10 @@ namespace Sandbox
if ( secs < 60 ) return string.Format( "{0} seconds", secs );
if ( m < 60 ) return string.Format( "{1} minutes, {0} seconds", secs % 60, m );
if ( h < 48 ) return string.Format( "{2} hours and {1} minutes", secs % 60, m % 60, h );
if ( d < 7 ) return string.Format( "{3} days, {2} hours and {1} minutes", secs % 60, m % 60, h % 24, d );
if ( h < 48 ) return string.Format( "{1} hours and {0} minutes", m % 60, h );
if ( d < 7 ) return string.Format( "{2} days, {1} hours and {0} minutes", m % 60, h % 24, d );
return string.Format( "{4} weeks, {3} days, {2} hours and {1} minutes", secs % 60, m % 60, h % 24, d % 7, w );
return string.Format( "{3} weeks, {2} days, {1} hours and {0} minutes", m % 60, h % 24, d % 7, w );
}
/// <inheritdoc cref=" FormatSecondsLong(long)"/>
public static string FormatSecondsLong( this ulong secs ) { return FormatSecondsLong( (long)secs ); }

View File

@@ -321,7 +321,7 @@ namespace Sandbox
internal static string FormatAssemblyName( Assembly asm )
{
return FormatAssemblyName( asm?.GetName() );
return asm is null ? string.Empty : FormatAssemblyName( asm.GetName() );
}
}
}

View File

@@ -343,7 +343,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Components
attribute.GloballyQualifiedTypeName = globallyQualifiedTypeName;
}
if ( attribute.BoundAttribute?.IsGenericTypedProperty() ?? false && attribute.TypeName != null )
if ( (attribute.BoundAttribute?.IsGenericTypedProperty() ?? false) && attribute.TypeName != null )
{
// If we know the type name, then replace any generic type parameter inside it with
// the known types.
@@ -368,7 +368,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Components
foreach ( var childContent in node.ChildContents )
{
if ( childContent.BoundAttribute?.IsGenericTypedProperty() ?? false && childContent.TypeName != null )
if ( (childContent.BoundAttribute?.IsGenericTypedProperty() ?? false) && childContent.TypeName != null )
{
// If we know the type name, then replace any generic type parameter inside it with
// the known types.

View File

@@ -104,10 +104,10 @@ public static partial class SandboxSystemExtensions
if ( secs < 60 ) return string.Format( "{0} seconds", secs );
if ( m < 60 ) return string.Format( "{1} minutes, {0} seconds", secs % 60, m );
if ( h < 48 ) return string.Format( "{2} hours and {1} minutes", secs % 60, m % 60, h );
if ( d < 7 ) return string.Format( "{3} days, {2} hours and {1} minutes", secs % 60, m % 60, h % 24, d );
if ( h < 48 ) return string.Format( "{1} hours and {0} minutes", m % 60, h );
if ( d < 7 ) return string.Format( "{2} days, {1} hours and {0} minutes", m % 60, h % 24, d );
return string.Format( "{4} weeks, {3} days, {2} hours and {1} minutes", secs % 60, m % 60, h % 24, d % 7, w );
return string.Format( "{3} weeks, {2} days, {1} hours and {0} minutes", m % 60, h % 24, d % 7, w );
}
/// <inheritdoc cref=" FormatSecondsLong(long)"/>
public static string FormatSecondsLong( this ulong secs ) { return FormatSecondsLong( (long)secs ); }