Skip to content

Commit 152fbf6

Browse files
Merge pull request #20 from TimeWarpEngineering/Cramer/2025-07-31/filename-rule
Fix TW0003 analyzer to properly handle editorconfig exclusions
2 parents a6534de + a41d626 commit 152fbf6

File tree

9 files changed

+232
-30
lines changed

9 files changed

+232
-30
lines changed

.vscode/settings.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,11 @@
1818
"titleBar.inactiveBackground": "#55dbe899",
1919
"titleBar.inactiveForeground": "#15202b99"
2020
},
21-
"peacock.remoteColor": "#55dbe8"
21+
"peacock.remoteColor": "#55dbe8",
22+
"cSpell.words": [
23+
"assemblyinfo",
24+
"buildtransitive",
25+
"contentfiles",
26+
"nugets"
27+
]
2228
}

Directory.Build.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
<Authors>Steven T. Cramer</Authors>
66
<Product>TimeWarp.SourceGenerators</Product>
77
<PackageId>TimeWarp.SourceGenerators</PackageId>
8-
<PackageVersion>1.0.0-beta.2</PackageVersion>
8+
<PackageVersion>1.0.0-beta.3</PackageVersion>
99
<PackageProjectUrl>https://timewarpengineering.github.io/timewarp-source-generators/</PackageProjectUrl>
1010
<PackageTags>TimeWarp; Source Generator;SourceGenerators; Delegate</PackageTags>
1111
<PackageIcon>logo.png</PackageIcon>

Directory.Packages.props

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
<PackageVersion Include="Microsoft.CodeAnalysis" Version="4.4.0" />
1010
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.11.0" />
1111
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.11.0" />
12+
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.11.0" />
13+
<PackageVersion Include="Microsoft.CodeAnalysis.Workspaces.Common" Version="4.11.0" />
1214
<PackageVersion Include="Microsoft.CSharp" Version="4.7.0" />
1315
<PackageVersion Include="Morris.Moxy" Version="1.1.0" />
1416
<PackageVersion Include="Scriban.Signed" Version="5.5.0" />
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# Fix TW0003 Analyzer to Ignore Generated Files
2+
3+
## Description
4+
The TW0003 file naming analyzer is incorrectly checking files in build output directories (obj/, bin/) which contain auto-generated build artifacts. The analyzer should skip validation for these directories as they are not user-written source code.
5+
6+
## Problem
7+
The analyzer is reporting errors for generated files such as:
8+
- `.NETCoreApp,Version=v10.0.AssemblyAttributes.cs` in obj/ directory
9+
- `timewarp-code.AssemblyInfo.cs` in obj/ directory
10+
11+
These files are created by the build system and their naming conventions are controlled by the .NET SDK, not the user.
12+
13+
## Acceptance Criteria
14+
- [ ] TW0003 analyzer skips files in `/obj/` directories
15+
- [ ] TW0003 analyzer skips files in `/bin/` directories
16+
- [ ] TW0003 analyzer skips files in other common build output directories
17+
- [ ] User source files continue to be validated correctly
18+
- [ ] Tests verify that generated files are ignored
19+
- [ ] Tests verify that regular source files are still checked
20+
21+
## Technical Details
22+
The fix should be implemented in the TW0003 analyzer by:
23+
1. Checking if the file path contains `/obj/` or `/bin/`
24+
2. Returning early without reporting diagnostics for these paths
25+
3. Consider using normalized path comparison to handle different path separators
26+
27+
## Implementation Location
28+
The fix should be applied in the TW0003 analyzer implementation, likely in the method that determines whether to analyze a given file.
29+
30+
## Test Cases
31+
- Verify files in obj/ directory are ignored
32+
- Verify files in bin/ directory are ignored
33+
- Verify nested obj/bin directories are ignored (e.g., `/src/obj/`)
34+
- Verify regular source files are still validated
35+
- Verify edge cases like files named "obj.cs" in source directories are still checked
36+
37+
## References
38+
- Original issue: `/home/steventcramer/worktrees/github.com/TimeWarpEngineering/timewarp-code/Cramer-2025-07-31-spike/analysis/tw0003-analyzer-issue.md`
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
namespace TimeWarp.SourceGenerators;
2+
3+
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(FileNameRuleCodeFixProvider)), Shared]
4+
public class FileNameRuleCodeFixProvider : CodeFixProvider
5+
{
6+
private const string Title = "Rename file to kebab-case";
7+
8+
public sealed override ImmutableArray<string> FixableDiagnosticIds =>
9+
ImmutableArray.Create(FileNameRuleAnalyzer.DiagnosticId);
10+
11+
public sealed override FixAllProvider GetFixAllProvider() =>
12+
WellKnownFixAllProviders.BatchFixer;
13+
14+
public sealed override Task RegisterCodeFixesAsync(CodeFixContext context)
15+
{
16+
var diagnostic = context.Diagnostics.First();
17+
var document = context.Document;
18+
19+
// Get the current file name
20+
var filePath = document.FilePath;
21+
if (string.IsNullOrEmpty(filePath))
22+
return;
23+
24+
var currentFileName = Path.GetFileName(filePath);
25+
var fileNameWithoutExtension = Path.GetFileNameWithoutExtension(currentFileName);
26+
var extension = Path.GetExtension(currentFileName);
27+
28+
// Convert to kebab-case
29+
var newFileNameWithoutExtension = ConvertToKebabCase(fileNameWithoutExtension);
30+
var newFileName = newFileNameWithoutExtension + extension;
31+
32+
// Only offer fix if the new name is different
33+
if (newFileName == currentFileName)
34+
return;
35+
36+
// Register the code fix
37+
context.RegisterCodeFix(
38+
CodeAction.Create(
39+
title: $"{Title}: '{newFileName}'",
40+
createChangedSolution: c => RenameFileAsync(document, newFileName, c),
41+
equivalenceKey: Title),
42+
diagnostic);
43+
44+
return Task.CompletedTask;
45+
}
46+
47+
private async Task<Solution> RenameFileAsync(
48+
Document document,
49+
string newFileName,
50+
CancellationToken cancellationToken)
51+
{
52+
var solution = document.Project.Solution;
53+
54+
// Get the new document with renamed file
55+
var newDocument = document.WithName(newFileName);
56+
57+
// Remove old document and add new one
58+
var newSolution = solution
59+
.RemoveDocument(document.Id)
60+
.AddDocument(
61+
newDocument.Id,
62+
newFileName,
63+
await document.GetTextAsync(cancellationToken),
64+
document.Folders,
65+
document.FilePath != null ? Path.Combine(Path.GetDirectoryName(document.FilePath)!, newFileName) : null);
66+
67+
return newSolution;
68+
}
69+
70+
private static string ConvertToKebabCase(string input)
71+
{
72+
if (string.IsNullOrEmpty(input))
73+
return input;
74+
75+
// Handle already kebab-case
76+
if (Regex.IsMatch(input, @"^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$"))
77+
return input;
78+
79+
var result = new StringBuilder();
80+
bool previousWasUpper = false;
81+
bool previousWasNumber = false;
82+
83+
for (int i = 0; i < input.Length; i++)
84+
{
85+
char c = input[i];
86+
87+
if (char.IsUpper(c))
88+
{
89+
// Add hyphen before uppercase letter if:
90+
// - Not at start
91+
// - Previous char was lowercase or number
92+
// - Or this is start of new word in acronym (next char is lowercase)
93+
if (i > 0 && (!previousWasUpper || (i + 1 < input.Length && char.IsLower(input[i + 1]))))
94+
{
95+
result.Append('-');
96+
}
97+
98+
result.Append(char.ToLowerInvariant(c));
99+
previousWasUpper = true;
100+
previousWasNumber = false;
101+
}
102+
else if (char.IsDigit(c))
103+
{
104+
// Add hyphen before number if previous wasn't number and we're not at start
105+
if (i > 0 && !previousWasNumber)
106+
{
107+
result.Append('-');
108+
}
109+
110+
result.Append(c);
111+
previousWasUpper = false;
112+
previousWasNumber = true;
113+
}
114+
else if (char.IsLower(c))
115+
{
116+
result.Append(c);
117+
previousWasUpper = false;
118+
previousWasNumber = false;
119+
}
120+
else if (c == '-' || c == '_')
121+
{
122+
// Replace underscores with hyphens, avoid double hyphens
123+
if (result.Length > 0 && result[result.Length - 1] != '-')
124+
{
125+
result.Append('-');
126+
}
127+
previousWasUpper = false;
128+
previousWasNumber = false;
129+
}
130+
}
131+
132+
// Clean up any double hyphens or trailing hyphens
133+
var cleaned = Regex.Replace(result.ToString(), @"-+", "-");
134+
cleaned = cleaned.Trim('-');
135+
136+
return cleaned;
137+
}
138+
}

kanban/to-do/015_create-kebab-case-file-name-code-fix.md renamed to kanban/to-do/015_create-kebab-case-file-name-code-fix/task.md

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
# Create Kebab-Case File Name Code Fix
22

3+
## Status: Blocked - Requires Separate Assembly
4+
5+
### Issue Discovered
6+
Code fix providers cannot be in the same assembly as source generators due to RS1038 error. The Microsoft.CodeAnalysis.Workspaces assembly (required for code fixes) is not provided during command line compilation scenarios.
7+
38
## Description
49
Create a code fix provider for the FileNameRuleAnalyzer (TW0003) that automatically renames files from PascalCase or other naming conventions to kebab-case format.
510

@@ -36,4 +41,18 @@ Create a code fix provider for the FileNameRuleAnalyzer (TW0003) that automatica
3641

3742
## Dependencies
3843
- Requires FileNameRuleAnalyzer (TW0003) to be working
39-
- Should follow existing code fix provider patterns in the codebase
44+
- Should follow existing code fix provider patterns in the codebase
45+
46+
## Implementation Progress
47+
- [x] Created FileNameRuleCodeFixProvider class
48+
- [x] Implemented PascalCase to kebab-case conversion logic
49+
- [x] Added file renaming through Roslyn workspace APIs
50+
- [ ] Blocked: Cannot include in same assembly as source generators
51+
52+
## Next Steps
53+
To complete this task, one of the following approaches is needed:
54+
1. Create a separate project for code fix providers (e.g., `timewarp-source-generators.codefixes`)
55+
2. Distribute code fixes as a separate NuGet package
56+
3. Restructure the solution to support both analyzers and code fixes properly
57+
58+
The code fix provider implementation is complete and stored in this folder for when the project structure supports it.

source/timewarp-source-generators/file-name-rule-analyzer.cs

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -30,26 +30,29 @@ public class FileNameRuleAnalyzer : IIncrementalGenerator
3030
"Directory.Build.props",
3131
"Directory.Build.targets",
3232
"Directory.Packages.props",
33-
"AssemblyInfo.cs",
33+
"*AssemblyInfo.cs",
34+
"*.AssemblyInfo.cs",
35+
"*.AssemblyAttributes.cs",
36+
"*.GlobalUsings.g.cs",
3437
"AnalyzerReleases.Shipped.md",
3538
"AnalyzerReleases.Unshipped.md"
3639
];
3740

3841
public void Initialize(IncrementalGeneratorInitializationContext context)
3942
{
4043
// Create a value provider that provides all syntax trees with config options
41-
var syntaxTreesWithConfig = context.CompilationProvider
44+
IncrementalValuesProvider<(SyntaxTree tree, AnalyzerConfigOptionsProvider configOptions)> syntaxTreesWithConfig = context.CompilationProvider
4245
.Combine(context.AnalyzerConfigOptionsProvider)
4346
.SelectMany((source, _) =>
4447
{
45-
var (compilation, configOptions) = source;
48+
(Compilation compilation, AnalyzerConfigOptionsProvider configOptions) = source;
4649
return compilation.SyntaxTrees.Select(tree => (tree, configOptions));
4750
});
4851

4952
// Register diagnostics for each syntax tree
5053
context.RegisterSourceOutput(syntaxTreesWithConfig, (spc, source) =>
5154
{
52-
var (tree, configOptions) = source;
55+
(SyntaxTree tree, AnalyzerConfigOptionsProvider configOptions) = source;
5356
AnalyzeFileNaming(spc, tree, configOptions);
5457
});
5558
}
@@ -69,7 +72,7 @@ private void AnalyzeFileNaming(SourceProductionContext context, SyntaxTree tree,
6972
return;
7073

7174
// Get configured exceptions
72-
string[] exceptions = GetConfiguredExceptions(configOptions);
75+
string[] exceptions = GetConfiguredExceptions(configOptions, tree);
7376

7477
// Check if file matches any exception pattern
7578
if (IsFileExcepted(fileName, exceptions))
@@ -78,28 +81,33 @@ private void AnalyzeFileNaming(SourceProductionContext context, SyntaxTree tree,
7881
// Check if file name follows kebab-case pattern
7982
if (!KebabCasePattern.IsMatch(fileName))
8083
{
81-
Location location = Location.Create(
84+
var location = Location.Create(
8285
tree,
83-
Microsoft.CodeAnalysis.Text.TextSpan.FromBounds(0, 0)
86+
TextSpan.FromBounds(0, 0)
8487
);
8588

86-
Diagnostic diagnostic = Diagnostic.Create(Rule, location, fileName);
89+
var diagnostic = Diagnostic.Create(Rule, location, fileName);
8790
context.ReportDiagnostic(diagnostic);
8891
}
8992
}
9093

91-
private string[] GetConfiguredExceptions(AnalyzerConfigOptionsProvider configOptions)
94+
private string[] GetConfiguredExceptions(AnalyzerConfigOptionsProvider configOptions, SyntaxTree tree)
9295
{
96+
// Get file-specific options
97+
AnalyzerConfigOptions options = configOptions.GetOptions(tree);
98+
9399
// Try to get configured exceptions from .editorconfig
94-
if (configOptions.GlobalOptions.TryGetValue(
100+
if (options.TryGetValue(
95101
"dotnet_diagnostic.TW0003.excluded_files",
96102
out string? configuredExceptions) && !string.IsNullOrEmpty(configuredExceptions))
97103
{
98104
// Split by semicolon and trim whitespace
99-
return configuredExceptions
100-
.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries)
101-
.Select(s => s.Trim())
102-
.ToArray();
105+
IEnumerable<string> additionalExceptions = configuredExceptions
106+
.Split([';'], StringSplitOptions.RemoveEmptyEntries)
107+
.Select(s => s.Trim());
108+
109+
// Merge defaults with configured exceptions
110+
return [.. DefaultExceptions, .. additionalExceptions];
103111
}
104112

105113
// Return default exceptions if not configured

tests/timewarp-source-generators-test-console/.editorconfig

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,8 @@
33
root = true
44

55
[*.cs]
6+
7+
dotnet_diagnostic.TW0003.severity = error # Set to warning/error to enforce kebab-case file naming
8+
dotnet_diagnostic.TW0003.excluded_files = PascalCaseTest.cs
69
# Enable TW0004 - XML documentation to markdown analyzer
710
dotnet_diagnostic.TW0004.severity = suggestion

tests/timewarp-source-generators-test-console/packages.lock.json

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"version": 2,
33
"dependencies": {
4-
"net10.0": {
4+
"net9.0": {
55
"Microsoft.CodeAnalysis.Analyzers": {
66
"type": "Direct",
77
"requested": "[3.11.0, )",
@@ -18,18 +18,6 @@
1818
"Microsoft.CodeAnalysis.Common": "[4.11.0]"
1919
}
2020
},
21-
"Microsoft.DotNet.ILCompiler": {
22-
"type": "Direct",
23-
"requested": "[10.0.0-preview.6.25358.103, )",
24-
"resolved": "10.0.0-preview.6.25358.103",
25-
"contentHash": "+bEoavvMKwx3xRAgUajaFiu3bdRuWCEhf+rkaubJM9jjq6Oj38y9eOlhMNwdnqg6FptLaL4DB79H/Y+A7V5nVw=="
26-
},
27-
"Microsoft.NET.ILLink.Tasks": {
28-
"type": "Direct",
29-
"requested": "[10.0.0-preview.6.25358.103, )",
30-
"resolved": "10.0.0-preview.6.25358.103",
31-
"contentHash": "+3mK1T4Y/M+u0fIlw3nDoTgOQGvVt3jPqpI9S8TBXFYJ1WnjCTbvwv+yLML9NyqaXpBg4jWV7EgIH9JWCOpa9Q=="
32-
},
3321
"Microsoft.CodeAnalysis.Common": {
3422
"type": "Transitive",
3523
"resolved": "4.11.0",

0 commit comments

Comments
 (0)