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
30 changes: 30 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -402,6 +402,36 @@ string envVars = File.ReadAllText(ProjectFiles._env);
- `.editorconfig` → `_editorconfig`
- `.txt` → `_txt`

**Important:** Avoid naming conflicts. The generator detects and reports duplicate property names:

**Example 1: Dot file conflict**
```xml
<!-- ❌ ERROR: Both generate property '_txt' -->
<ItemGroup>
<None Update=".txt">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="_txt">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
```

**Example 2: Dots vs underscores conflict**
```xml
<!-- ❌ ERROR: Both generate property 'config_json' -->
<ItemGroup>
<None Update="config.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="config_json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
```

Error: `PROJFILES004: Files 'config.json' and 'config_json' both generate the same property name 'config_json'. Rename one of the files to avoid the conflict.`


## Glob Pattern Support

Expand Down
8 changes: 8 additions & 0 deletions src/ProjectFiles/Diagnostics.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,12 @@ public static class Diagnostics
category: "ProjectFiles",
DiagnosticSeverity.Error,
isEnabledByDefault: true);

public static readonly DiagnosticDescriptor DuplicatePropertyName = new(
id: "PROJFILES004",
title: "Duplicate property name generated",
messageFormat: "Files '{0}' and '{1}' both generate the same property name '{2}'. Rename one of the files to avoid the conflict.",
category: "ProjectFiles",
DiagnosticSeverity.Error,
isEnabledByDefault: true);
}
59 changes: 58 additions & 1 deletion src/ProjectFiles/Generator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,21 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
context.ReportDiagnostic(diagnostic);
}

// Check for duplicate property names
var duplicateConflicts = FindDuplicatePropertyNames(fileList);
foreach (var (file1, file2, propertyName) in duplicateConflicts)
{
conflictingFiles.Add(file1);
conflictingFiles.Add(file2);
var diagnostic = Diagnostic.Create(
Diagnostics.DuplicatePropertyName,
Location.None,
file1,
file2,
propertyName);
context.ReportDiagnostic(diagnostic);
}

// Filter out conflicting files before generating source
var filteredFiles = fileList.Where(_ => !conflictingFiles.Contains(_)).ToImmutableArray();

Expand Down Expand Up @@ -152,6 +167,48 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
return conflicts;
}

static List<(string File1, string File2, string PropertyName)> FindDuplicatePropertyNames(ImmutableArray<string> files)
{
var conflicts = new List<(string, string, string)>();

// Group files by their directory (same scope)
var filesByDirectory = new Dictionary<string, List<string>>();

foreach (var file in files)
{
var directory = Path.GetDirectoryName(file) ?? string.Empty;
if (!filesByDirectory.TryGetValue(directory, out var filesInDir))
{
filesInDir = [];
filesByDirectory[directory] = filesInDir;
}

filesInDir.Add(file);
}

// Check for duplicates within each directory
foreach (var filesInDir in filesByDirectory.Values)
{
var propertyToFile = new Dictionary<string, string>();

foreach (var file in filesInDir)
{
var propertyName = ToFilePropertyName(file);

if (propertyToFile.TryGetValue(propertyName, out var existingFile))
{
conflicts.Add((existingFile, file, propertyName));
}
else
{
propertyToFile[propertyName] = file;
}
}
}

return conflicts;
}

static string GenerateSource(ImmutableArray<string> files, MsBuildProperties properties, Cancel cancel)
{
var (tree, rootFiles) = BuildFileTree(files, cancel);
Expand Down Expand Up @@ -417,4 +474,4 @@ static string ToFilePropertyName(string filePath)

return (topLevelDirectories.Values, rootFiles);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
//HintName: ProjectFiles.ProjectDirectory.g.cs
namespace ProjectFilesGenerator;

using System.IO;
using System.Collections.Generic;

partial class ProjectDirectory(string path)
{
public string Path { get; } = path;

public string FullPath => System.IO.Path.GetFullPath(Path);

public override string ToString() => Path;

public static implicit operator string(ProjectDirectory temp) =>
temp.Path;

public static implicit operator FileInfo(ProjectDirectory temp) =>
new(temp.Path);

public IEnumerable<string> EnumerateDirectories() =>
Directory.EnumerateDirectories(Path);

public IEnumerable<string> EnumerateFiles() =>
Directory.EnumerateFiles(Path);

public IEnumerable<string> GetFiles() =>
Directory.GetFiles(Path);

public IEnumerable<string> GetDirectories() =>
Directory.GetDirectories(Path);

public DirectoryInfo Info => new(Path);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
//HintName: ProjectFiles.ProjectFile.g.cs
namespace ProjectFilesGenerator;

using System.IO;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

partial class ProjectFile(string path)
{
public string Path { get; } = path;

public string FullPath => System.IO.Path.GetFullPath(Path);

public override string ToString() => Path;

public static implicit operator string(ProjectFile temp) =>
temp.Path;

public static implicit operator FileInfo(ProjectFile temp) =>
new(temp.Path);

public FileStream OpenRead() =>
File.OpenRead(Path);

public StreamReader OpenText() =>
File.OpenText(Path);

public string ReadAllText() =>
File.ReadAllText(Path);

public string ReadAllText(Encoding encoding) =>
File.ReadAllText(Path, encoding);

public FileInfo Info => new(Path);

#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP2_0_OR_GREATER
public Task<string> ReadAllTextAsync(CancellationToken cancel = default) =>
File.ReadAllTextAsync(Path, cancel);

public Task<string> ReadAllTextAsync(Encoding encoding, CancellationToken cancel = default) =>
File.ReadAllTextAsync(Path, encoding,cancel);
#else
public Task<string> ReadAllTextAsync(CancellationToken cancel = default) =>
Task.FromResult(File.ReadAllText(Path));

public Task<string> ReadAllTextAsync(Encoding encoding, CancellationToken cancel = default) =>
Task.FromResult(File.ReadAllText(Path, encoding));
#endif
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
//HintName: ProjectFiles.g.cs
// <auto-generated/>
#nullable enable

namespace ProjectFilesGenerator
{
using ProjectFilesGenerator.Types;

/// <summary>Provides strongly-typed access to project files marked with CopyToOutputDirectory.</summary>
static partial class ProjectFiles
{
}
}

namespace ProjectFilesGenerator.Types
{
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
Diagnostics: [
{
Message: Files '.txt' and '_txt' both generate the same property name '_txt'. Rename one of the files to avoid the conflict.,
Severity: Error,
Descriptor: {
Id: PROJFILES004,
Title: Duplicate property name generated,
MessageFormat: Files '{0}' and '{1}' both generate the same property name '{2}'. Rename one of the files to avoid the conflict.,
Category: ProjectFiles,
DefaultSeverity: Error,
IsEnabledByDefault: true
}
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
//HintName: ProjectFiles.ProjectDirectory.g.cs
namespace ProjectFilesGenerator;

using System.IO;
using System.Collections.Generic;

partial class ProjectDirectory(string path)
{
public string Path { get; } = path;

public string FullPath => System.IO.Path.GetFullPath(Path);

public override string ToString() => Path;

public static implicit operator string(ProjectDirectory temp) =>
temp.Path;

public static implicit operator FileInfo(ProjectDirectory temp) =>
new(temp.Path);

public IEnumerable<string> EnumerateDirectories() =>
Directory.EnumerateDirectories(Path);

public IEnumerable<string> EnumerateFiles() =>
Directory.EnumerateFiles(Path);

public IEnumerable<string> GetFiles() =>
Directory.GetFiles(Path);

public IEnumerable<string> GetDirectories() =>
Directory.GetDirectories(Path);

public DirectoryInfo Info => new(Path);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
//HintName: ProjectFiles.ProjectFile.g.cs
namespace ProjectFilesGenerator;

using System.IO;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

partial class ProjectFile(string path)
{
public string Path { get; } = path;

public string FullPath => System.IO.Path.GetFullPath(Path);

public override string ToString() => Path;

public static implicit operator string(ProjectFile temp) =>
temp.Path;

public static implicit operator FileInfo(ProjectFile temp) =>
new(temp.Path);

public FileStream OpenRead() =>
File.OpenRead(Path);

public StreamReader OpenText() =>
File.OpenText(Path);

public string ReadAllText() =>
File.ReadAllText(Path);

public string ReadAllText(Encoding encoding) =>
File.ReadAllText(Path, encoding);

public FileInfo Info => new(Path);

#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP2_0_OR_GREATER
public Task<string> ReadAllTextAsync(CancellationToken cancel = default) =>
File.ReadAllTextAsync(Path, cancel);

public Task<string> ReadAllTextAsync(Encoding encoding, CancellationToken cancel = default) =>
File.ReadAllTextAsync(Path, encoding,cancel);
#else
public Task<string> ReadAllTextAsync(CancellationToken cancel = default) =>
Task.FromResult(File.ReadAllText(Path));

public Task<string> ReadAllTextAsync(Encoding encoding, CancellationToken cancel = default) =>
Task.FromResult(File.ReadAllText(Path, encoding));
#endif
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
//HintName: ProjectFiles.g.cs
// <auto-generated/>
#nullable enable

namespace ProjectFilesGenerator
{
using ProjectFilesGenerator.Types;

/// <summary>Provides strongly-typed access to project files marked with CopyToOutputDirectory.</summary>
static partial class ProjectFiles
{
}
}

namespace ProjectFilesGenerator.Types
{
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
Diagnostics: [
{
Message: Files 'config.json' and 'config_json' both generate the same property name 'config_json'. Rename one of the files to avoid the conflict.,
Severity: Error,
Descriptor: {
Id: PROJFILES004,
Title: Duplicate property name generated,
MessageFormat: Files '{0}' and '{1}' both generate the same property name '{2}'. Rename one of the files to avoid the conflict.,
Category: ProjectFiles,
DefaultSeverity: Error,
IsEnabledByDefault: true
}
}
]
}
Loading