Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 36 additions & 3 deletions .github/workflows/diff-generated-code.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ jobs:
- name: Setup .NET
uses: actions/setup-dotnet@baa11fbfe1d6520db94683bd5c7a3818018e4309 # v5
with:
dotnet-version: '8.0.x'
dotnet-version: '10.0.x'

- name: Build base branch to generate KnownMimeTypes.cs
working-directory: base-branch
Expand All @@ -38,9 +38,42 @@ jobs:
- name: Generate diff
id: diff
run: |
echo "=== DEBUG: Searching for generated files ==="
echo "Base branch files:"
find base-branch -name "KnownMimeTypes.cs" -o -name "KnownMimeTypes.g.cs" 2>/dev/null || echo "No files found"
echo ""
echo "PR branch files:"
find pr-branch -name "KnownMimeTypes.cs" -o -name "KnownMimeTypes.g.cs" 2>/dev/null || echo "No files found"
echo ""

# Find the generated file (supports both old T4 and new source generator locations)
BASE_FILE=$(find base-branch -name "KnownMimeTypes.cs" -o -name "KnownMimeTypes.g.cs" 2>/dev/null | head -1)
PR_FILE=$(find pr-branch -name "KnownMimeTypes.cs" -o -name "KnownMimeTypes.g.cs" 2>/dev/null | head -1)

echo "=== DEBUG: Selected files ==="
echo "BASE_FILE: $BASE_FILE"
echo "PR_FILE: $PR_FILE"
echo ""

if [ -z "$BASE_FILE" ] || [ -z "$PR_FILE" ]; then
echo "ERROR: Could not find generated files!"
echo "Listing all .cs files in obj directories:"
find base-branch -path "*/obj/*" -name "*.cs" 2>/dev/null | head -20
find pr-branch -path "*/obj/*" -name "*.cs" 2>/dev/null | head -20
exit 1
fi

echo "=== DEBUG: File sizes ==="
wc -l "$BASE_FILE" "$PR_FILE"
echo ""

# Extract only the public const string lines and diff them
grep 'public const string .* = "' base-branch/src/MimeMapping/KnownMimeTypes.cs | sort > base-consts.txt
grep 'public const string .* = "' pr-branch/src/MimeMapping/KnownMimeTypes.cs | sort > pr-consts.txt
grep 'public const string .* = "' "$BASE_FILE" | sort > base-consts.txt
grep 'public const string .* = "' "$PR_FILE" | sort > pr-consts.txt

echo "=== DEBUG: Extracted constants count ==="
wc -l base-consts.txt pr-consts.txt
echo ""

diff -u base-consts.txt pr-consts.txt > diff.txt || true

Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -274,3 +274,4 @@ __pycache__/
# Cake - Uncomment if you are using it
# tools/
KnownMimeTypes.cs
mime-db.json
16 changes: 15 additions & 1 deletion MimeMapping.sln
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@


Copy link
Collaborator

Choose a reason for hiding this comment

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

we should convert this to slnx in a follow up for simpler maintenance

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.26430.16
Expand All @@ -14,6 +14,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MimeMapping.Tests", "test\M
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MimeMapping", "src\MimeMapping\MimeMapping.csproj", "{42893F4D-DE5F-4132-A408-E90BFF840342}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MimeMapping.SourceGenerator", "src\MimeMapping.SourceGenerator\MimeMapping.SourceGenerator.csproj", "{B5E3D7A1-8C4F-4F2E-9A1B-3C5D7E9F1A2B}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -48,6 +50,18 @@ Global
{42893F4D-DE5F-4132-A408-E90BFF840342}.Release|x64.Build.0 = Release|Any CPU
{42893F4D-DE5F-4132-A408-E90BFF840342}.Release|x86.ActiveCfg = Release|Any CPU
{42893F4D-DE5F-4132-A408-E90BFF840342}.Release|x86.Build.0 = Release|Any CPU
{B5E3D7A1-8C4F-4F2E-9A1B-3C5D7E9F1A2B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B5E3D7A1-8C4F-4F2E-9A1B-3C5D7E9F1A2B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B5E3D7A1-8C4F-4F2E-9A1B-3C5D7E9F1A2B}.Debug|x64.ActiveCfg = Debug|Any CPU
{B5E3D7A1-8C4F-4F2E-9A1B-3C5D7E9F1A2B}.Debug|x64.Build.0 = Debug|Any CPU
{B5E3D7A1-8C4F-4F2E-9A1B-3C5D7E9F1A2B}.Debug|x86.ActiveCfg = Debug|Any CPU
{B5E3D7A1-8C4F-4F2E-9A1B-3C5D7E9F1A2B}.Debug|x86.Build.0 = Debug|Any CPU
{B5E3D7A1-8C4F-4F2E-9A1B-3C5D7E9F1A2B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B5E3D7A1-8C4F-4F2E-9A1B-3C5D7E9F1A2B}.Release|Any CPU.Build.0 = Release|Any CPU
{B5E3D7A1-8C4F-4F2E-9A1B-3C5D7E9F1A2B}.Release|x64.ActiveCfg = Release|Any CPU
{B5E3D7A1-8C4F-4F2E-9A1B-3C5D7E9F1A2B}.Release|x64.Build.0 = Release|Any CPU
{B5E3D7A1-8C4F-4F2E-9A1B-3C5D7E9F1A2B}.Release|x86.ActiveCfg = Release|Any CPU
{B5E3D7A1-8C4F-4F2E-9A1B-3C5D7E9F1A2B}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down
10 changes: 2 additions & 8 deletions renovate.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,8 @@
{
"customType": "regex",
"description": "Update mime-db",
"managerFilePatterns": [
"/(^|/)KnownMimeTypes\\.tt$/",
"/(^|/)TemplateSourceTests\\.cs$/",
"/(^|/)MimeDbTestHelper\\.cs$/"
],
"matchStrings": [
"https://raw\\.githubusercontent\\.com/jshttp/mime-db/(?<currentValue>.+?)/db.json"
],
"managerFilePatterns": ["/(^|/)MimeMapping\\.csproj$/"],
"matchStrings": ["<MimeDbVersion>(?<currentValue>.+?)</MimeDbVersion>"],
"datasourceTemplate": "github-releases",
"depNameTemplate": "mime-db",
"packageNameTemplate": "jshttp/mime-db"
Expand Down
184 changes: 184 additions & 0 deletions src/MimeMapping.SourceGenerator/CodeGenerator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
using System.Text;

namespace MimeMapping.SourceGenerator
{
/// <summary>
/// Generates the KnownMimeTypes.cs source code from parsed MIME data
/// </summary>
internal static class CodeGenerator
{
public static string Generate(MimeDbData data)
{
var sb = new StringBuilder();

// Header
sb.AppendLine("using System;");
sb.AppendLine();
sb.AppendLine("#nullable enable");
sb.AppendLine();
sb.AppendLine("namespace MimeMapping");
sb.AppendLine("{");

// Class documentation
sb.AppendLine(" ///<summary>");
sb.AppendLine($" /// MIME type constants. Last updated on {data.GeneratedAt:s}Z. ");
sb.AppendLine($" /// Generated from the <a href=\"{data.SourceUrl}\">mime-db</a> source");
sb.AppendLine(" ///</summary>");
sb.AppendLine(" public static class KnownMimeTypes");
sb.AppendLine(" {");
sb.AppendLine();

// Conflict resolution comments
foreach (var comment in data.ConflictComments)
{
sb.AppendLine($" {comment}");
}

// Summary comments
sb.AppendLine();
sb.AppendLine($" // Generated {data.MimeTypeToExtensions.Count} unique mime type values");
sb.AppendLine($" // Generated {data.ExtensionToMimeType.Count} type key pairs");
sb.AppendLine();

// Source URL constant
sb.AppendLine(" ///<summary>The source URL of the mime-db data used to generate this file</summary>");
sb.AppendLine($" internal const string MimeDbSourceUrl = \"{data.SourceUrl}\";");
sb.AppendLine();

// MIME type constants
foreach (var kv in data.ExtensionToMimeType)
{
var fieldName = NameUtilities.GetMimeFieldName(kv.Key);
sb.AppendLine($" ///<summary>{kv.Key}</summary>");
sb.AppendLine($" public const string {fieldName} = \"{kv.Value}\";");
}

// ALL_MIMETYPES lazy array
sb.AppendLine(" // List of all available mimetypes, used to build the dictionary");
sb.AppendLine(" internal static readonly Lazy<string[]> ALL_MIMETYPES = new Lazy<string[]>(() => new [] {");
foreach (var kv in data.ExtensionToMimeType)
{
var fieldName = NameUtilities.GetMimeFieldName(kv.Key);
sb.AppendLine($" {fieldName},");
}
sb.AppendLine(" });");
sb.AppendLine();
sb.AppendLine();

// FileExtensions nested class
sb.AppendLine(" ///<summary>File extensions</summary>");
sb.AppendLine(" public static class FileExtensions");
sb.AppendLine(" {");
foreach (var kv in data.ExtensionToMimeType)
{
var fieldName = NameUtilities.GetExtensionFieldName(kv.Key);
sb.AppendLine($" ///<summary>{kv.Key}</summary>");
sb.AppendLine($" public const string {fieldName} = \"{kv.Key}\";");
}
sb.AppendLine(" }");
sb.AppendLine();

// ALL_EXTS lazy array
sb.AppendLine(" // List of all available extensions, used to build the dictionary");
sb.AppendLine(" internal static readonly Lazy<string[]> ALL_EXTS = new Lazy<string[]>(() => new [] {");
foreach (var kv in data.ExtensionToMimeType)
{
var fieldName = NameUtilities.GetExtensionFieldName(kv.Key);
sb.AppendLine($" FileExtensions.{fieldName},");
}
sb.AppendLine(" });");
sb.AppendLine();
sb.AppendLine();

// LookupType switch statement
GenerateLookupTypeMethod(sb, data);

// LookupMimeType switch statement
GenerateLookupMimeTypeMethod(sb, data);

// Close class and namespace
sb.AppendLine(" }");
sb.AppendLine("}");

return sb.ToString();
}

private static void GenerateLookupTypeMethod(StringBuilder sb, MimeDbData data)
{
sb.AppendLine(" // Switch-case instead of dictionary since it does the hashing at compile time rather than run time");
sb.AppendLine(" internal static string? LookupType(string type)");
sb.AppendLine(" {");
sb.AppendLine(" switch (type)");
sb.AppendLine(" {");

foreach (var kv in data.MimeTypeToExtensions)
{
var mimeType = kv.Key;
var extensions = kv.Value;

// Generate case statements for all extensions that map to this MIME type
foreach (var ext in extensions)
{
var fieldName = NameUtilities.GetExtensionFieldName(ext);
sb.AppendLine($" case FileExtensions.{fieldName}:");
}

// Return the MIME type constant (use first extension's field name)
var firstFieldName = NameUtilities.GetMimeFieldName(extensions[0]);
sb.AppendLine($" return {firstFieldName};");
sb.AppendLine();
}

sb.AppendLine(" default: ");
sb.AppendLine(" return null;");
sb.AppendLine(" }");
sb.AppendLine(" }");
sb.AppendLine();
}

private static void GenerateLookupMimeTypeMethod(StringBuilder sb, MimeDbData data)
{
sb.AppendLine(" // Switch-case instead of dictionary since it does the hashing at compile time rather than run time");
sb.AppendLine(" internal static string[]? LookupMimeType(string mimeType)");
sb.AppendLine(" {");
sb.AppendLine(" switch (mimeType)");
sb.AppendLine(" {");

foreach (var kv in data.MimeTypeToExtensions)
{
var extensions = kv.Value;
var first = true;

// Generate case statements for each extension's MIME type constant
foreach (var ext in extensions)
{
var fieldName = NameUtilities.GetMimeFieldName(ext);
if (first)
{
sb.AppendLine($" case {fieldName}:");
first = false;
}
else
{
// Comment out additional cases (they're duplicates pointing to same MIME type)
sb.AppendLine($" //case {fieldName}:");
}
}

// Return array of extension constants
var extFieldNames = new StringBuilder();
for (int i = 0; i < extensions.Count; i++)
{
if (i > 0) extFieldNames.Append(", ");
extFieldNames.Append($"FileExtensions.{NameUtilities.GetExtensionFieldName(extensions[i])}");
}
sb.AppendLine($" return new[] {{{extFieldNames}}};");
}

sb.AppendLine(" default: ");
sb.AppendLine(" return null;");
sb.AppendLine(" }");
sb.AppendLine(" }");
}
}
}
78 changes: 78 additions & 0 deletions src/MimeMapping.SourceGenerator/KnownMimeTypesGenerator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
using System;
using System.Linq;
using Microsoft.CodeAnalysis;

namespace MimeMapping.SourceGenerator
{
/// <summary>
/// Source generator that produces KnownMimeTypes.cs from mime-db JSON data
/// </summary>
[Generator]
public class KnownMimeTypesGenerator : IIncrementalGenerator
{
private const string MimeDbFileName = "mime-db.json";
private const string MimeDbUrlPropertyName = "build_property.MimeDbUrl";
private const string DefaultSourceUrl = "mime-db";

public void Initialize(IncrementalGeneratorInitializationContext context)
{
// Find the mime-db.json from AdditionalFiles
var mimeDbProvider = context.AdditionalTextsProvider
.Where(file => file.Path.EndsWith(MimeDbFileName, StringComparison.OrdinalIgnoreCase))
.Select((file, ct) => file.GetText(ct)?.ToString())
.Where(content => !string.IsNullOrEmpty(content))
.Collect()
.Select((contents, ct) => contents.FirstOrDefault());

// Get the MimeDbUrl from global options for documentation
var optionsProvider = context.AnalyzerConfigOptionsProvider
.Select((provider, ct) =>
{
provider.GlobalOptions.TryGetValue(MimeDbUrlPropertyName, out var url);
return url ?? DefaultSourceUrl;
});

// Combine and generate
var combined = mimeDbProvider.Combine(optionsProvider);

context.RegisterSourceOutput(combined, (spc, tuple) =>
{
var (json, sourceUrl) = tuple;
if (string.IsNullOrEmpty(json))
{
// Report diagnostic if mime-db.json not found
spc.ReportDiagnostic(Diagnostic.Create(
new DiagnosticDescriptor(
"MIME001",
"mime-db.json not found",
"The mime-db.json file was not found in AdditionalFiles. Ensure the DownloadMimeDb MSBuild target runs before compilation.",
"MimeMapping.SourceGenerator",
DiagnosticSeverity.Error,
isEnabledByDefault: true),
Location.None));
return;
}

try
{
var data = MimeDbParser.Parse(json!, sourceUrl);
var source = CodeGenerator.Generate(data);
spc.AddSource("KnownMimeTypes.g.cs", source);
}
catch (Exception ex)
{
spc.ReportDiagnostic(Diagnostic.Create(
new DiagnosticDescriptor(
"MIME002",
"Failed to parse mime-db.json",
"Failed to parse mime-db.json: {0}",
"MimeMapping.SourceGenerator",
DiagnosticSeverity.Error,
isEnabledByDefault: true),
Location.None,
ex.Message));
}
});
}
}
}
Loading