From dae7c274cf56e0e8022ce0b29c0157b904eaca5a Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Sat, 14 May 2022 11:09:40 -0600 Subject: [PATCH] Detect and report suspicious characters in NativeMethods.txt Closes #392 --- src/Microsoft.Windows.CsWin32/Generator.cs | 24 ++++++++ .../SourceGenerator.cs | 13 ++++- .../GeneratorTests.cs | 58 +++++++++++++++++++ 3 files changed, 94 insertions(+), 1 deletion(-) diff --git a/src/Microsoft.Windows.CsWin32/Generator.cs b/src/Microsoft.Windows.CsWin32/Generator.cs index 3dc0e048..7569b53d 100644 --- a/src/Microsoft.Windows.CsWin32/Generator.cs +++ b/src/Microsoft.Windows.CsWin32/Generator.cs @@ -441,6 +441,30 @@ group method by GetClassNameForModule(entry.Key) into x private string DebuggerDisplayString => $"Generator: {this.InputAssemblyName}"; + /// + /// Tests whether a string contains characters that do not belong in an API name. + /// + /// The user-supplied string that was expected to match some API name. + /// if the string contains characters that are likely mistakenly included and causing a mismatch; otherwise. + public static bool ContainsIllegalCharactersForAPIName(string apiName) + { + for (int i = 0; i < apiName.Length; i++) + { + char ch = apiName[i]; + bool allowed = false; + allowed |= char.IsLetterOrDigit(ch); + allowed |= ch == '_'; + allowed |= ch == '.'; // for qualified name searches + + if (!allowed) + { + return true; + } + } + + return false; + } + /// public void Dispose() { diff --git a/src/Microsoft.Windows.CsWin32/SourceGenerator.cs b/src/Microsoft.Windows.CsWin32/SourceGenerator.cs index 539b6431..1b772b75 100644 --- a/src/Microsoft.Windows.CsWin32/SourceGenerator.cs +++ b/src/Microsoft.Windows.CsWin32/SourceGenerator.cs @@ -36,6 +36,14 @@ public class SourceGenerator : ISourceGenerator DiagnosticSeverity.Warning, isEnabledByDefault: true); + public static readonly DiagnosticDescriptor NoMatchingMethodOrTypeWithBadCharacters = new DiagnosticDescriptor( + "PInvoke001", + "No matching method, type or constant found", + "Method, type or constant \"{0}\" not found. It contains unexpected characters, possibly including invisible characters, which can happen when copying and pasting from docs.microsoft.com among other places. Try deleting the line and retyping it.", + "Functionality", + DiagnosticSeverity.Warning, + isEnabledByDefault: true); + public static readonly DiagnosticDescriptor NoMatchingMethodOrTypeWithSuggestions = new DiagnosticDescriptor( "PInvoke001", "No matching method, type or constant found", @@ -315,7 +323,10 @@ void ReportNoMatch(Location? location, string failedAttempt) } else { - context.ReportDiagnostic(Diagnostic.Create(NoMatchingMethodOrType, location, failedAttempt)); + context.ReportDiagnostic(Diagnostic.Create( + Generator.ContainsIllegalCharactersForAPIName(failedAttempt) ? NoMatchingMethodOrTypeWithBadCharacters : NoMatchingMethodOrType, + location, + failedAttempt)); } } } diff --git a/test/Microsoft.Windows.CsWin32.Tests/GeneratorTests.cs b/test/Microsoft.Windows.CsWin32.Tests/GeneratorTests.cs index bf0514cc..9b3758cc 100644 --- a/test/Microsoft.Windows.CsWin32.Tests/GeneratorTests.cs +++ b/test/Microsoft.Windows.CsWin32.Tests/GeneratorTests.cs @@ -7,6 +7,7 @@ using System.IO; using System.Linq; using System.Reflection; +using System.Reflection.Metadata; using System.Runtime.InteropServices; using System.Text; using System.Threading; @@ -2500,6 +2501,63 @@ public void OpensMetadataForSharedReading() Assert.True(this.generator.TryGenerate("CreateFile", CancellationToken.None)); } + [Fact] + public void ContainsIllegalCharactersForAPIName_InvisibleCharacters() + { + // You can't see them, but there are invisible hyphens in this name. + // Copy-paste from docs.microsoft.com has been known to include these invisible characters and break matching in NativeMethods.txt. + Assert.True(Generator.ContainsIllegalCharactersForAPIName("SHGet­Known­Folder­Item")); + } + + [Fact] + public void ContainsIllegalCharactersForAPIName_DisallowedVisibleCharacters() + { + Assert.True(Generator.ContainsIllegalCharactersForAPIName("Method-1")); + } + + [Fact] + public void ContainsIllegalCharactersForAPIName_LegalNames() + { + Assert.False(Generator.ContainsIllegalCharactersForAPIName("SHGetKnownFolderItem")); + Assert.False(Generator.ContainsIllegalCharactersForAPIName("Method1")); + Assert.False(Generator.ContainsIllegalCharactersForAPIName("Method_1")); + Assert.False(Generator.ContainsIllegalCharactersForAPIName("Qualified.Name")); + } + + [Fact] + public void ContainsIllegalCharactersForAPIName_AllActualAPINames() + { + using FileStream metadataStream = File.OpenRead(MetadataPath); + using System.Reflection.PortableExecutable.PEReader peReader = new(metadataStream); + MetadataReader metadataReader = peReader.GetMetadataReader(); + foreach (MethodDefinitionHandle methodDefHandle in metadataReader.MethodDefinitions) + { + MethodDefinition methodDef = metadataReader.GetMethodDefinition(methodDefHandle); + string methodName = metadataReader.GetString(methodDef.Name); + Assert.False(Generator.ContainsIllegalCharactersForAPIName(methodName), methodName); + } + + foreach (TypeDefinitionHandle typeDefHandle in metadataReader.TypeDefinitions) + { + TypeDefinition typeDef = metadataReader.GetTypeDefinition(typeDefHandle); + string typeName = metadataReader.GetString(typeDef.Name); + if (typeName == "") + { + // Skip this special one. + continue; + } + + Assert.False(Generator.ContainsIllegalCharactersForAPIName(typeName), typeName); + } + + foreach (FieldDefinitionHandle fieldDefHandle in metadataReader.FieldDefinitions) + { + FieldDefinition fieldDef = metadataReader.GetFieldDefinition(fieldDefHandle); + string fieldName = metadataReader.GetString(fieldDef.Name); + Assert.False(Generator.ContainsIllegalCharactersForAPIName(fieldName), fieldName); + } + } + private static string ConstructGlobalConfigString(bool omitDocs = false) { StringBuilder globalConfigBuilder = new();